TS decorators¶
decorator¶
- decorators are used in meta programming.
- usually, decorators are used in the development more effective, and have no effects on the end user.
- decorators can be used for classes or class members.
- decorators on methods, are similar to higher-order functions, they take a function and return a new function with some additional behavior (metadata or meta-behavior).
- decorators on classes, are similar to mixins, they take a class and return a new class with some additional behavior (metadata or meta-behavior).
-
class members that can be decorated, are:
- class itself (the constructor specifically).
- class methods.
- class properties.
- class getters/setters.
- class static methods.
- parameters of class methods.
-
in each of the previous cases, the parameters that the decorator takes, are different, docs: https://www.typescriptlang.org/docs/handbook/decorators.html
- Enable decorators in tsconfig by set
"experimentalDecorators": true
- decorators will be called only once, when the class is declared and NOT when the an object is instantiated from the class .
- once the class declared, the code that is generated by calling decorators will be injected into the class.
-
there are 2 types of decorators:
- normal decorators, can be called using
@decorator
syntax. you can’t pass custom arguments to the decorator. - decorator factories, can be called using
@decorator(args)
syntax. you can pass custom arguments to the decorator.
- normal decorators, can be called using
function ClassDecoratorLogger(logPrefix: string) {
return (f: Function) => {
console.log(logPrefix + " :: " + f.name);
};
}
function MethodDecoratorLogger(targetProto: object, functionName: string, descriptor: TypedPropertyDescriptor<any>) {
console.log({ targetProto, functionName, descriptor });
return descriptor;
}
function MethodDecoratorLoggerCallable<T extends Record<string, any>>(logString: string) {
return (target: T, functionName: string, descriptor: TypedPropertyDescriptor<any>) => {
console.log(logString + " :: " + functionName);
// target[functionName].prototype.log = function () {
// console.log(logString + " :: " + this.name);
// };
console.log({ [functionName]: target[functionName] });
return descriptor;
};
}
@ClassDecoratorLogger("my class")
class MyClass {
@MethodDecoratorLogger
add(a: number, b: number) => a + b;
@MethodDecoratorLoggerCallable<MyClass>("my method")
subtract(a: number, b: number) => a - b;
}
const myObject = new MyClass();
myObject.subtract(1, 2);
myObject.add(1, 2);
Replacing a class with new class using decorator¶
- the decorator can return a class that replaces the original class.
- the logic of the decorator will be injected into the newly returned class.
type AdditionalLogic = {
// type ofv the logic that decorator will add to the class
instantiated: boolean;
func: () => void;
};
function InstantiatingLogger(logPrefix: string) {
return function <T extends { new (...args: any[]): Record<string, any> }>(originalConstructor: T) {
// generic class type that can work for any class we pass to the decorator `{ new (...args: any[]): Record<string, any>`
return class extends originalConstructor {
// extending the original class
constructor(...args: any[]) {
super(...args); // call the original constructor
console.log(logPrefix + " :: " + this.name);
this.instantiated = true; // add a property to the newly returned class
}
func() {
// add a method to the newly returned class
// do something
console.log("calling new func created by decorator on " + this.name);
}
} as unknown as T & AdditionalLogic;
};
}
@InstantiatingLogger("creating a new instance of person ...")
class Person {
constructor(public name: string) {}
}
const ahmad = new Person("Ahmad") as Person & AdditionalLogic; // creating a new instance of person ... logPrefix :: Ahmad
const ali = new Person("Ali") as Person & AdditionalLogic; // creating a new instance of person ... logPrefix :: Ali
ahmad.func(); // calling new func created by decorator on Ahmad
replacing a method with a new method using decorator¶
- for decorator on methods, you can return a function that replaces the original method.
- the decorator gets the descriptor of the method, and can modify it, then return the new descriptor.
function AutoBind(target: any, propertyName: string | symbol) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor {
console.log("method decorator");
const originalMethod = descriptor.value;
const adjustedDescriptor: PropertyDescriptor = {
configurable: true,
enumerable: false,
get() {
// always bind this to the original method's context
return originalMethod.bind(this);
},
};
return adjustedDescriptor;
};
}
class MyClass {
messageToDisplay = "Hello World";
@AutoBind
interactWithDomListener(event: Event) {
console.log("interacting with dom");
console.log(this.messageToDisplay);
document.querySelector("#message").innerHTML = this.messageToDisplay;
}
}
const myObject = new MyClass();
const button = document.querySelector("#button");
button.addEventListener("click", myObject.interactWithDomListener); //without decorator, this will be bound to the window object
// with the decorator, this will be bound to the myObject object, thus the messageToDisplay will be accessible