Table of contents
1.
Introduction
2.
Purpose of Using Decorators in Typescript
2.1.
Decorator Factories
2.2.
Decorator Composition
3.
Types of Decorators in Typescript
3.1.
Class Decorators
3.2.
Method Decorators
3.3.
Accessor Decorators
3.4.
Property Decorators
3.5.
Parameter Decorators
4.
Frequently Asked Questions
5.
Key Takeaway
Last Updated: Mar 27, 2024

Decorators in Typescript

Author Aditya Kumar
0 upvote
Career growth poll
Do you think IIT Guwahati certified course can help you in your career?

Introduction

A decorator is a type of declaration that can decorate classes, methods, accessors, properties, and parameters. Decorators are just functions with the @expression symbol prefixed, where expression must evaluate to a function that will be invoked with information about the decorated declaration at runtime.

Decorators are an ES7 feature that has been proposed as an experiment. Some JavaScript frameworks, including Angular 2, are already using it. In future editions, the Decorators may change.

We must activate the experimentalDecorators compiler option on the command line or in our tsconfig.json to enable experimental support for decorators:

Command Line

$tsc --target ES5 --experimentalDecorators  

tsconfig.json

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

 

Purpose of Using Decorators in Typescript

The objective of Decorators in typescript is to add annotations and metadata to existing code in a declarative manner.

Decorator Factories

We can write a decorator factory to customise how a decorator is applied to a declaration. A decorator factory is a function that returns the expression that the decorator will use when it is called at runtime.

The following is an example of a decorator factory:

function getColor(value: string) {
  // This is where the decorator factory sets up.
  // the decorator function that was returned
  return function (target) {
    // decorator
    // Make something out of the words 'target' and 'value'...
  };
}

Decorator Composition

A declaration can have many decorators applied to it. The following examples can assist you in understanding it.

Using only one line

@f @g x

Using Multiple Lines

@f
@g
x

The evaluation of many decorators on a single declaration is comparable to function composition in mathematics. When composing the functions f and g in this form, the resulting composite (f o g)(x) is equivalent to f(g(x)).

When evaluating several decorators on a single declaration in TypeScript, the steps are as follows:

  • Each decorator's expressions are evaluated from top to bottom.
  • From bottom to top, the outcomes are referred to as functions.

We can see this evaluation order with the following example if we utilise decorator factories:

function myFirst() {
  console.log("myFirst Function: factory evaluated");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("myFirst Function: Called Here!");
  };
}
 
function mySecond() {
  console.log("mySecond Function: factory evaluated");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("mySecond Function: Called Here!");
  };
}
 
class ExampleClass {
  @myFirst()
  @mySecond()
  method() {}
}
 

This output would be printed to the console using the following command:

myFirst Function: factory evaluated
mySecond Function: factory evaluated
mySecond Function: Called Here!
myFirst Function: Called Here!

Types of Decorators in Typescript

Decorators in TypeScript are used in the following ways:

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

Class Decorators

A class decorator speaks about the class behaviours and is defined shortly before the class declaration. The constructor of the class is decorated with a class decorator. A class decorator can be used to look at, change, or replace the definition of a class. If the class decorator returns a value, the specified function Object() { [native code] } function will be used to replace the class declaration.

Example

function SelfDriving(constructorFunction: Function) {
    console.log('— the decorator function has been called —');
    constructorFunction.prototype.selfDrivable = true;
}

@SelfDriving
class Car {
    private _make: string;
    constructor(make: string) {
        console.log('This constructor was called —');
        this._make = make;
    }
}
console.log('— establishing a new instance -');
let car: Car = new Car("Thar");
console.log(car);
console.log(`selfDriving: ${car['selfDrivable']}`);

console.log('— adding one more instance —');
car = new Car("Bugati");
console.log(car);
console.log(`selfDriving: ${car['selfDrivable']}`); 

Output:

Method Decorators

Just before a method declaration, a method decorator is defined. It's applied to the method's property descriptor. It can be used to look at, change, or replace the definition of a method. In a declaration file, we can't use the method decorator.

The method decorator function takes three arguments in its expression. They are as follows:

  1. For a static member, the class's constructor function or its prototype for an instance member.
  2. The name of a member.
  3. This is the member's Property Descriptor.

Example

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

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

Output

Accessor Decorators

Just before an accessor declaration, an Accessor Decorator is specified. It is applied to the accessor's property descriptor. It can be used to inspect, change, or replace the definitions of an accessor.

The accessor decorator function takes three arguments in its statement. They are as follows:

  1. Either the class's constructor function for a static member of the class's prototype for an instance member.
  2. The name of the member.
  3. The member's Property Descriptor.

Example

function Enumerable(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    //enumerable method
    descriptor.enumerable = true;
}

class Person {
    _name: string;

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

    @Enumerable
    get name() {
        return this._name;
    }
}

console.log("-- creating instance --");
let person = new Person("Diana");
console.log("-- looping --");
for (let key in person) {
    console.log(key + " = " + person[key]);
}  

Output

Property Decorators

Just before a property declaration, a property decorator is defined. It's analogous to the decorators for methods. Property decorators vary from method decorators in that they don't take a property descriptor as an input and don't return anything.

The property decorator function takes two arguments in its statement. They are as follows:

  1. For a static member, the class's constructor function or its prototype for an instance member.
  2. The name of a member.

Example

In the example below, the @ReadOnly decorator makes the name property read-only, preventing us from changing its value.

const mylistOnly = (mylist: string[]) => {
  return (target: any, memberName: string) => {
    let currentValue: any = target[memberName];

    Object.defineProperty(target, memberName, {
      set: (newValue: any) => {
        if (!mylist.includes(newValue)) {
          return;
        }
        currentValue = newValue;
      },
      get: () => currentValue
    });
  };
}

class Person {
  @mylistOnly(["Hemsworth", "Parker"])
  name: string = "Hemsworth";
}

const person = new Person();
console.log(person.name);
person.name = "John";
console.log(person.name);
person.name = "Parker";
console.log(person.name);

Output:

Parameter Decorators

Just before a parameter declaration, a parameter decorator is defined. It's used for a class function Object() { [native code] } or method declaration function. It can't be utilised in a declaration file or anywhere else in the environment (such as in a declared class).

The parameter decorator function accepts three arguments in its expression. They are as follows:

  1. For a static member, the class's constructor function or its prototype for an instance member.
  2. The name of a member.
  3. The parameter's index is in the function's arguments list.

Example

A parameter decorator (@required) is applied to a parameter of a member of the Person class in the example below.

function notNull(target: any, propertyKey: string, parameterIndex: number) {
    console.log("The notNull method of the param decorator has been called. ");
    Validator.registerNotNull(target, propertyKey, parameterIndex);
}

function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("The validate function of the method decorator has been called. ");
    let originalMethod = descriptor.value;
    //wrapup the actual method
    descriptor.value = function (...args: any[]) {//wrap methop
        if (!Validator.performValidation(target, propertyKey, args)) {
            console.log("Validation failure so method aborted: " + propertyKey);
            return;
        }
        let result = originalMethod.apply(this, args);
        return result;
    }
}

class Validator {
    private static notNullValidatorMap: Map<any, Map<string, number[]>> = new Map();

    //more validator maps should be included
    static registerNotNull(target: any, methodName: string, paramIndex: number): void {
        let paramMap: Map<string, number[]> = this.notNullValidatorMap.get(target);
        if (!paramMap) {
            paramMap = new Map();
            this.notNullValidatorMap.set(target, paramMap);
        }
        let paramIndexes: number[] = paramMap.get(methodName);
        if (!paramIndexes) {
            paramIndexes = [];
            paramMap.set(methodName, paramIndexes);
        }
        paramIndexes.push(paramIndex);
    }

    static performValidation(target: any, methodName: string, paramValues: any[]): boolean {
        let notNullMethodMap: Map<string, number[]> = this.notNullValidatorMap.get(target);
        if (!notNullMethodMap) {
            return true;
        }
        let paramIndexes: number[] = notNullMethodMap.get(methodName);
        if (!paramIndexes) {
            return true;
        }
        let hasErrors: boolean = false;
        for (const [index, paramValue] of paramValues.entries()) {
            if (paramIndexes.indexOf(index) != -1) {
                if (!paramValue) {
                    console.error("at index method param " + index + " cannot be null");
                    hasErrors = true;
                }
            }
        }
        return !hasErrors;
    }
}

class Task {
    @validate
    run(@notNull name: string): void {
        console.log("running task, name: " + name);
    }
}

console.log("-- creating instance --");
let task: Task = new Task();
console.log("-- calling Task#run(null) --");
task.run(null);
console.log("----------------");
console.log("-- calling Task#run('test') --");
task.run("test");

Output

Frequently Asked Questions

1. How do we perform Decorator evaluation?

Ans: The order in which decorators are applied to various declarations within a class is well defined:

  • For each instance member, parameter decorators are applied first, followed by Method, Accessor, or Property Decorators.
  • For each static member, parameter decorators are applied first, followed by Method, Accessor, or Property Decorators.
  • For the constructor, parameter decorators are used.
  • The class is decorated with class decorators.

 

2. What is a Decorator?

Ans: A decorator is a type of declaration that can be added to a class declaration, a method, an accessor, a property, or a parameter. Decorators have the form @expression, where expression must evaluate to a function that will be called with information about the decorated declaration when it is called at runtime.

For example, given the decorator @sealed, the sealed function could be written as follows:

function sealed(target) {
  // perform something for target
}

Key Takeaway

We have discussed decorators in typescript, used them with classes, and learnt the differences between them in this article. We can now start developing our decorators to reduce boilerplate code in our codebase, or we can utilise decorators more confidently with libraries like Mobx.

You can take a look at our Typescript archives section and see many more interesting topics related to it.

Apart from that, you can refer to our guided paths on Coding Ninjas Studio to learn more about DSA, Competitive Programming, JavaScript, and System Design.

Happy Learning!

Live masterclass