1hr 17min Section
Module Introduction
Useful for meta-programming, not so much for end-users but for other developers.
Example: guarantee one of our classes gets used in a certain way or ensure a method is called, etc.
The TS docs have a concise explanation here.
A First Class Decorator
When getting started with decorator code you will get a TS warning on your class, “Experimental support for decorators is a feature that is subject to change in a future release. Set the ‘experimentalDecorators’ option in your ‘tsconfig’ or ‘jsconfig’ to remove this warning.”
So tweak the “tsconfig.json” file with “target” set to “esnext” or “es6” and makes sure to uncomment out or add the “experimentalDecorators”: true line.
"experimentalDecorators": true
Note: If using NextJS, you mays till get a warning: Support for the experimental syntax “decorators-legacy” isn’t currently enabled. Installing this package here (@babel/plugin-proposal-decorators) and tweaking the “.babelrc” file did the trick.
Decorator is a function you apply to something, like class, in a certain way.
Start with capital letter although this is not necessary
Leverage the decorator with the “@” symbol, a special identifier that TS recognizes points to a function (our target function) without parentheses because we are pointing and not calling.”
Decorators execute when a class is defined and not when it is instantiated. You don’t need to instantiate class at all and the decorator code will still run.
// base case
function Logger (constructor: Function) {
console.log('Logging...')
console.log(constructor)
}
@Logger
class Person {
name: 'Max';
constructor() {
console.log("Creating person object...")
}
}
Note: the argument “constructor” is essentially the target.
Working with Decorator Factories
Besides using the base case we can use a factory function.
Here is the factory function: A function that returns a new anonymous function that takes in the constructor argument.
// factory function decorator example
function Logger(logString: string) {
return function((constructor: Function) {
console.log(logString);
console.log(constructor);
}
}
@Logger('')
class Person {
name: 'Max';
constructor('LOGGING - PERSON') {
console.log("Creating person object...")
}
}
Why do we do this? Advantage is we can pass in values as arguments, whichever ones we want and as many as you want. These values that will be used by the inner function. More power, more possibilities.
Building More Useful Decorators
A more useful example named “withTemplate” that renders html into a DOM node using innerHTML.
function withTemplate(template: string, hookId: string) {
return function(constructor: Function) {
const hookEl = document.getElementById(hookId)
if (hookEl) {
hookEl.innerHTML = template;
}
}
}
@withTemplate('<h1>My Person Object</h1>', 'app')
class Person {
name = Max;
constructor() {
}
}
Not interested in the “constructor” argument so tell TS you are not interested in using it with an underscore. This signals to TS I know I am aware of it, but I don’t need it and won’t be using it.
return function(_: Function) {}
Angular uses decorators to pass in object where you specify templates and render to the DOM.
The “meta programming” of decorators provides extra utilities to other developers.
Adding Multiple Decorators
You can add more than one decorator to a class. In which order do they execute? The execution of the actual decorator functions execute bottom up. Factory functions happen top to bottom. From the TS docs: “The expressions for each decorator are evaluated top-to-bottom. The results are then called as functions from bottom-to-top.”
Diving into Property Decorators
Can add decorators to properties and if so the decorator gets two arguments, the first is the target and the second is the property name.
function Log(target: any, propertyName: string | Symbol) {
console.log('Property Decorator');
console.log(target, propertyName);
}
class Product {
@Log
title: string;
private _price: number;
constructor(t: string, p: number) {
this.title = t;
this._price = p;
}
setPrice(val: number) {
// don't accept negative prices...
if (val > 0) {
this._price = val;
} else {
throw new Error("Invalid price - should be positive")
}
}
getPriceWithTax(tax: number) {
return this._price * (1 + tax)
}
}
Note that the decorator above runs when the class is defined, and more specifically when the property is defined.
Accessor & Parameter Decorators
Can add decorators to accessors (setters and getters). This decorator function gets a third argument that is a “PropertyDescriptor” a type built into TS):
function Log2 (target: any, name: string, descriptor: PropertyDescriptor) {...}
Can also add decorators to methods. A method decorator also receives three arguments. They are the same as for the accessors.
function Log3 (target: any, name: string | Symbol, descriptor: PropertyDescriptor) {...}
Can also add a decorator to a parameter. Don’t have to add to all parameters. The first argument is the target, (same as before), the second argument is the name of the method in which we use, and the third argument is now the position (as an index) of the argument.
function Log3 (target: any, name: string | Symbol, position: number) {...}
When Do Decorators Execute?
All of these above are running without instantiating. Decorators are executed when a class is defined, not when the class is instantiated. Decorators add extra functionality behind the scenes. Can add extra logic to run later but this isn’t always set up this way.
Returning (and changing) a Class in a Class Decorator
Some decorators are capable of returning, a return value inside of the decorator function.
What you can return depends on which type of decorator you are working with.
When working with a “class decorator” you can return a new constructor function that is based on the original constructor function and we can add some functionality with a new constructor function by calling “super()” Insert mind-blown-emoji or inception-movie-gif here.
Very confusing example in this chapter. Used the decorator to extend the base class and adds some new functionality. Probably the most complex TypeScript code I’ve seen.
function withTemplate(template, string, hookId: string) {
console.log("TEMPLATE FACTORY")
return function<T extends {new(..args: any[]): {name: string}}>(originalConstructor: T) {
return class extends originalConstructor {
constructor(..._: any[]) {
super()
// add whatever new logic to base class
console.log("Rendering Template")
const hookEl = document.getElementById(hookId)
if (hookEl) {
hookEl.innerHTML = template
hookEl.querySelector("h1")!.textContent = this.name
}
}
}
}
}
Note: Assign special type with “new” property (reserved keyword.)
Since we call “super()” we save what is in original class.
This example shows how we can use a decorator to replace an existing class with a custom class that extends and replaces the old class and runs extra custom logic when the class is instantiated.
Other Decorator Return Types
Cannot add return values to all decorators. Can add in decorators used on methods and accessor.
Return values not supported or used with decorators used on properties and parameters.
Note: Property Descriptors are a Vanilla JS thing and can define properties in more detail, (configurable, enumerable, get, and set, value, and writeable, etc.). Look more into this concept. PropertyDescriptor is also a built-in TS type.
Example shown in next chapter of returning something with the help of a method decorator.
Example: Creating an “Autobind” Decorator
Can return something, like a description, allows to change method, or configuration of the method.
Add a button that executes a method on an object.
Create a “Printer” class with a “showMessage” method. Instantiate the “Printer” class and add a button.
Could add “bind” method to help control the “this” with the event listener and prevent it from changing to window or an another object. In the example he creates an “Autobind” decorator, (we make up this name, it could be called anything) that will keep the “this” keyword pointing to the right object automatically.
In the “Autobind” decorator function we want to make sure “this” is always pointing to the object that the method belongs to.
Note: you can set “enumerable” to false so it won’t show up in “for in” loops.
Add a “get” to add some extra logic before we execute the function. It’s like a property with extra functionality. The “getter” will stand as an extra layer in our example here.
“return adjDescriptor”
Not interested in the first and second parameters so using the “underscore” concept.
One neat example of decorators.
Validation with Decorators – First Steps
“Course” example
Sometime we fetch data but we don’t know for sure.
We let users create the data but not sure if they do it right.
Hook up listener to the form and add exclamation mark
Use type-casting
Convert price to number with the “plus sign trick”
Also works if it has empty fields
Can add an if check
Would be nice if the validation logic was in the “course class”, We will later see a package that does this in a much more advanced way.
Required function
PostivieNumber” functon
validate function logic
“store” the data some where.
Validation with Decorators – Finished
Begin implement with interface “ValidatorConfig”
Create a new “registeredValidator” constant variable
Fill in “Required” function
Fill in “PositiveNumber” function
Work on “Validation” function.
“objValidatorConfig” constant variable.
“for in” loop to go through all validators
Have a switch built on (validator)
“return” true if object props is truthy and use double bang operator.
Back to the switch handle the “required” case and the “positive” case
To find the bug add a console log. the price is the first property that gets validated.
Need and “isValid” variable.
Also a problem because we override and we should add existing with the spread operator.
Checks include:
First “naive” implementation of validation with decorators.
Fixing a Validator Bug
In the current form, our validation logic is not entirely correct. It’s not working as intended.
At the moment, only one validator value is stored in the array (e.g. 'required'
) – of course that’s not what we need. Multiple values should be stored instead – at least potentially.
Here’s how you can adjust the code to make that work:
const registeredValidators: ValidatorConfig = {};
function Required(target: any, propName: string) {
registeredValidators[target.constructor.name] = {
...registeredValidators[target.constructor.name],
[propName]: [...(registeredValidators[target.constructor.name]?.[propName] ?? []), 'required']
};
}
function PositiveNumber(target: any, propName: string) {
registeredValidators[target.constructor.name] = {
...registeredValidators[target.constructor.name],
[propName]: [...(registeredValidators[target.constructor.name]?.[propName] ?? []), 'positive']
};
}
Wrap Up
Decorators work, configure, what you can do. Shows complexity but the power. Lots of pre-existing decorators out there.
The “TS Class validator” package ( install with “class validator –save-dev”) is a validation package and add.
Angular framework embraces decorators
Nest.JS framework embrace decorators
Used for meta-programming.
Useful Resources & Links