React, Typescript, Tailwind CSS

Typescript Decorators Unwrapped

Decorators in TypeScript are a special kind of declaration that can be attached to a class, method, accessor, property, or parameter. Decorators are a stage 2 proposal for JavaScript and are available as an experimental feature of TypeScript. They provide a way to add metadata, modify behavior, and manage cross-cutting concerns in a declarative manner


Introduction

Decorators are denoted by an @ symbol followed by the decorator name and cane be placed immediately before the declaration they are decorating. They are nothing but functions which are being applied to the target, and they receive the information about the target as parameters.

Enabling Experimental Decorators?

To use decorators in TypeScript, you need to enable the experimental feature in your tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

Types of Decorators

There are several types of decorators in TypeScript:

  1. Class Decorators
  2. Method Decorators
  3. Accessor Decorators
  4. Property Decorators
  5. Parameter Decorators

Let's go through examples of each type.

Class Decorators

A class decorator is a function that is applied to the class constructor. It can be used to observe, modify, or replace a class definition. They receives the constructor of the class as its target. We can use these to add additional properties or methods to a class, apply mixins, or modify class metadata.

// Sealing an object prevents extensions and makes existing properties non-configurable.
function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

@sealed
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return `Hello, ${this.greeting}`;
    }
}
// Yet another example
function classDecorator(constructor: Function) {
  // Add a new property to the class
  constructor.prototype.newProperty = 'Hello, world!';
}

@classDecorator
class MyClass {
  // ...
}

const instance = new MyClass();
console.log(instance.newProperty); // Output: Hello, world

Method Decorators

A method decorator is applied to a method of a class. It receives the class prototype, the name of the method, and the property descriptor. We can use these to perform actions before or after the method is called, modify the method's parameters or return value, or add additional logic.

function log(target: any, propertyName: string, descriptor: PropertyDescriptor) {
    const method = descriptor.value;
    descriptor.value = function(...args: any[]) {
        console.log(`Calling ${propertyName} with arguments: ${args.join(', ')}`);
        const result =  method.apply(this, args);
        console.log(`Method ${propertyName} returned: ${result}`);
        return result;
    };
}

class Calculator {
    @log
    add(a: number, b: number): number {
        return a + b;
    }
}

const calculator = new Calculator();
console.log(calculator.add(2, 3));  // Output: Calling add with arguments: 2, 3
                                    //         Method add returned: 5
                                    //         5

Accessor Decorators

Accessor decorators are applied to the getters and setters of class properties. They receive the class prototype, the name of the property, and the property descriptor. Accessor decorators can be used to modify or add metadata to a getter or setter within a class.

function accessorDecorator(target: any, propertyName: string, descriptor: PropertyDescriptor) {
  const originalGetter = descriptor.get;

  descriptor.get = function () {
    console.log(`Getting ${propertyName}`);
    return originalGetter.call(this);
  };

  const originalSetter = descriptor.set;

  descriptor.set = function (value: any) {
    console.log(`Setting ${propertyName} to: ${value}`);
    originalSetter.call(this, value);
  };
}

class MyClass {
  private _myProperty: string;

  @accessorDecorator
  get myProperty(): string {
    return this._myProperty;
  }

  set myProperty(value: string) {
    this._myProperty = value;
  }
}

const instance = new MyClass();
instance.myProperty = 'Hello, world!'; // Output: Setting myProperty to: Hello, world!
console.log(instance.myProperty); // Output: Getting myProperty

Property Decorators

Property decorators are applied to class properties. They receive the class prototype and the name of the property. They do not have access to the property descriptor. We can use these to add additional behavior or metadata to a property.

function format(formatString: string) {
    return function(target: any, propertyKey: string) {
        let value: string;

        const getter = function() {
            return value;
        };

        const setter = function(newValue: string) {
            value = `${formatString} ${newValue}`;
        };

        Object.defineProperty(target, propertyKey, {
            get: getter,
            set: setter,
            enumerable: true,
            configurable: true,
        });
    };
}

class Customer {
    @format('Customer:')
    name: string;

    constructor(name: string) {
        this.name = name;
    }
}

const customer = new Customer('John Doe');
console.log(customer.name);  // Output: Customer: John Doe

Property Decorators

Property decorators are applied to class properties. They receive the class prototype and the name of the property. They do not have access to the property descriptor. We can these to perform actions or validation on the parameter value, or add additional metadata.

function logParameter(target: any, propertyName: string, parameterIndex: number) {
  const args = target[propertyName].toString().match(/\(([^)]*)\)/)[1].split(', ');
  console.log(`Parameter ${parameterIndex} of ${propertyName} in ${target.constructor.name} is ${args[parameterIndex]}`);
}

class Logger {
  log(@logParameter name: string) {
    console.log(`Hello, ${name}!`);
  }
}

const logger = new Logger();
logger.log('Sunny'); // Output: Parameter 0 of log in Logger is name
                     //         Hello, Sunny!

Decorators Execution Order

When multiple decorators are applied to the same target (class, method, property, etc.), the order in which they are executed follows a specific pattern.

function classDecorator1(target: any) {
  console.log("Executing class decorator 1");
}

function classDecorator2(target: any) {
  console.log("Executing class decorator 2");
}

function methodDecorator1(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log("Executing method decorator 1");
}

function methodDecorator2(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log("Executing method decorator 2");
}

function propertyDecorator1(target: any, propertyKey: string) {
  console.log("Executing property decorator 1");
}

function propertyDecorator2(target: any, propertyKey: string) {
  console.log("Executing property decorator 2");
}

function parameterDecorator1(target: any, propertyKey: string | symbol, parameterIndex: number) {
  console.log("Executing parameter decorator 1");
}

function parameterDecorator2(target: any, propertyKey: string | symbol, parameterIndex: number) {
  console.log("Executing parameter decorator 2");
}

function accessorDecorator1(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log("Executing accessor decorator 1");
}

function accessorDecorator2(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log("Executing accessor decorator 2");
}

@classDecorator1
@classDecorator2
class ExampleClass {
  @propertyDecorator1
  @propertyDecorator2
  exampleProperty: string;

  constructor(
    @parameterDecorator1 parameter1: string,
    @parameterDecorator2 parameter2: number
  ) {}

  @methodDecorator1
  @methodDecorator2
  exampleMethod() {}

  @accessorDecorator1
  @accessorDecorator2
  get exampleAccessor() {
    return this.exampleProperty;
  }

  @accessorDecorator1
  @accessorDecorator2
  set exampleAccessor(value: string) {
    this.exampleProperty = value;
  }
}

// Output

// Executing property decorator 2
// Executing property decorator 1
// Executing method decorator 2
// Executing method decorator 1
// Executing accessor decorator 2
// Executing accessor decorator 1
// Executing parameter decorator 2
// Executing parameter decorator 1
// Executing class decorator 2
// Executing class decorator 1
  • Decorators of the same type that are closer to the target are executed first. For example, in the case of property decorators, @propertyDecorator2 is executed before @propertyDecorator1. This same rule applies to all types of decorators.

  • The order of execution for different types of decorators is as follows:

    1. Property Decorators
    2. Method Decorators
    3. Accessor Decorators
    4. Parameter Decorators
    5. Class Decorators
  • There is a well defined order to how decorators applied to various declarations inside of a class are applied:

    • Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each instance member.
    • Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each static member.
    • Parameter Decorators are applied for the constructor.
    • Class Decorators are applied for the class.

Applications of Decorators

Logging: Decorators can be used to add logging functionality to functions or methods. For example, we can create a log decorator that logs the input arguments and return value of a function.

Authentication: They can be used to enforce authentication checks before allowing access to certain functions or methods. For example, we can create an authenticate decorator that verifies the user's authentication status before executing a method.

Validation: Decorators can be used to perform input validation on function parameters. For example, we can create a validate decorator that checks if the arguments meet certain criteria.

Memoization: Decorators can be used to implement memoization, which is a technique that caches the results of expensive function calls for better performance. For example, we can create a memoize decorator that caches the return value based on the function arguments.

Dependency Injection:- Decorators can be used to handle dependency injection, where dependencies are automatically injected into a class or function. For example, we can create an inject decorator that automatically injects dependencies based on metadata.

Previous
Typescript magic code snippets