Skip to content

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:

    1. class itself (the constructor specifically).
    2. class methods.
    3. class properties.
    4. class getters/setters.
    5. class static methods.
    6. 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:

    1. normal decorators, can be called using @decorator syntax. you can’t pass custom arguments to the decorator.
    2. decorator factories, can be called using @decorator(args) syntax. you can pass custom arguments to the decorator.
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

Packages that use decorators