The Ultimate guide to Decorators in Typescript

The Ultimate guide to Decorators in Typescript

Decorators mean better edge of your TS Sword

·

5 min read

Play this article

What are Decorators

Decorators are a powerful feature in TypeScript that allows developers to modify or extend the behaviour of classes, methods, accessors, and properties. They offer an elegant way to add functionality or modify the behaviour of existing constructs without altering their original implementation.

More formally -

A Decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Decorators use the form @expression, where the expression must evaluate to a function that will be called at runtime with information about the decorated declaration.

The History of Decorators

Decorators have a rich history in the TypeScript and JavaScript ecosystems. The concept of decorators was inspired by Python and other programming languages. The initial decorator's proposal for JavaScript was introduced in 2014, and since then, several versions of the proposal have been developed, with the current one being at stage 3 of the ECMAScript standardization process which is now officially supported by Typescript 5.0.

To enable experimental support for decorators, you must enable the experimentalDecorators compiler option either on the command line or in your tsconfig.json:

Command Line:

tsc --target ES5 --experimentalDecorators

tsconfig.json:

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}

Decorator Functions and Their Capabilities

A decorator is a function that takes the construct being decorated as its argument and may return a modified version of the construct or a new construct altogether. Decorators can be used to:

  • Modify the behaviour of a class, method, accessor, or property

  • Add new functionality to a class or method

  • Provide metadata for a construct

  • Enforce coding standards or best practices

Decorator Factories

If we want to customize how a decorator is applied to a declaration, we can write a decorator factory. A Decorator Factory is simply a function that returns the expression that will be called by the decorator at runtime.

We can write a decorator factory in the following fashion:

function color(value: string) {  
// this is the decorator factory, it sets up  
// the returned decorator function  
    return function (target) {    
        // this is the decorator    
        // do something with 'target' and 'value'...  
    };
}

Decorator Evaluation

There is a well-defined order to how decorators applied to various declarations inside of a class are applied:

  1. Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each instance member.

  2. Parameter Decorators, followed by Method, Accessor, or Property Decorators are applied for each static member.

  3. Parameter Decorators are applied for the constructor.

  4. Class Decorators are applied for the class.

Types of Decorators

Decorators can be categorised into the following types -

  1. Class Decorators

  2. Method Decorators

  3. Getter and Setter (Property) Decorators

  4. Field (Parameter) Decorators

  5. Auto-accessor Decorators

Class Decorators

Class decorators are applied to class constructors and can be used to modify or extend the behaviour of a class. Some common use cases for class decorators include:

  • Collecting instances of a class

  • Freezing instances of a class

  • Making classes function-callable

function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class BugReport {
  type = "report";
  title: string;

  constructor(t: string) {
    this.title = t; // cannot perform this action now.
  }
}

Method Decorators

Method decorators are applied to class methods and can be used to modify or extend the behaviour of a method. Some common use cases for method decorators include:

  • Tracing method invocations

  • Binding methods to instances

  • Applying functions to methods

function Enumerable(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log(target);
    console.log("-- proertyKey -- ", propertyKey);
    console.log("-- descriptor -- ", descriptor);
    //make the method enumerable
    descriptor.enumerable = true;
}

class Car {
    @Enumerable
    run() {
        console.log("inside run method...");
    }
}
console.log("-- creating instance --");
let car = new Car();
console.log("-- looping --");
for (let key in car) {
    console.log("key: " + key);
}

Getter and Setter Decorators

Getter and setter decorators are applied to class accessors, allowing developers to modify or extend their behaviour. Common use cases for getter and setter decorators include:

  • Compute values lazily and cache

  • Implementing read-only properties

  • Validating property assignments

function lazy(target: (this: This) => Return, context: ClassGetterDecoratorContext) {
  return function (this: This): Return {
    const value = target.call(this);
    Object.defineProperty(this, context.name, { value, enumerable: true });
    return value;
  };
}

class MyClass {
  private _expensiveValue: number | null = null;

  @lazy
  get expensiveValue(): number {
    this._expensiveValue ??= computeExpensiveValue();
    return this._expensiveValue;
  }
}

function computeExpensiveValue(): number {
  // Expensive computation here…
  console.log('computing...'); // Only call once
  return 42;
}

const obj = new MyClass();

console.log(obj.expensiveValue);
console.log(obj.expensiveValue);
console.log(obj.expensiveValue);

Field Decorators

Field decorators are applied to class fields and can be used to modify or extend the behaviour of a field. Common use cases for field decorators include:

  • Changing initialization values of fields

  • Implementing read-only fields

  • Dependency injection

  • Emulating enums

function addOne<T>(target: undefined, context: ClassFieldDecoratorContext<T, number>) {
  return function (this: T, value: number) {
    console.log('addOne: ', value); // 3
    return value + 1;
  };
}

function addTwo<T>(
  target: undefined,
  context: ClassFieldDecoratorContext<T, number>
) {
  return function (this: T, value: number) {
    console.log('addTwo: ', value); // 1
    return value + 2;
  };
}


class MyClass {
  @addOne
  @addTwo
  x = 1;
}

console.log(new MyClass().x); // 4

Note: multiple decorators are evaluated in the bottom to the top order.

Auto-Accessor Decorators

Auto-accessors are a new language feature that simplifies the creation of getter and setter pairs. It helps avoid problems that occur when decorator authors try to replace instance fields with accessors on the prototype, because ECMAScript instance fields shadow accessors when they are mounted on the instance.

function logged(value, context) {
  const { kind, name } = context;
  const fieldName = String(name);
  if (kind === "accessor") {
    let { get, set } = value;

    return {
      get(this) {
        console.log(`Getting ${fieldName}`);
        return get.call(this);
      },

      set(this, val) {
        console.log(`Setting ${fieldName} to ${val}`);
        return set.call(this, val);
      },

      init(initialValue) {
        console.log(`Initializing ${fieldName} with value ${initialValue}`);
        return initialValue;
      },
    };
  }
}

class User {
  @logged
  accessor name: string = "Anonymous";

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

const user = new User("John Doe");
console.log(user.name);

Conclusion

Decorators are a powerful feature in TypeScript that can help you to write better, and compact code.

As of now, Decorators are a Stage 2 proposal for JavaScript and a Phase 3 proposal in Typescript. As the decorator's proposal continues to evolve and mature, you can gradually choose to apply it to your project.

You can find more information at - https://github.com/microsoft/TypeScript/pull/50820

Did you find this article valuable?

Support Utkarsh by becoming a sponsor. Any amount is appreciated!