TypeScript - Decorators



Decorators in TypeScript are special kinds of declarations that can be attached to the class declaration, property, accessor, method, and parameters. It is used for the separate modification of the class without modifying the original source code. This makes them a powerful tool in the domain of object-oriented programming, allowing you to write cleaner, more organized code that adheres to the DRY (Don't Repeat Yourself) principle.

Using Decorators in TypeScript

You are required to enable the 'experimentalDecorators' compiler option to enable the experimental support for decorators in your TypeScript project.

There are 2 ways to enable 'experimentalDecorators' compiler option in TypeScript. You can use any option.

  • Execute the below command in the terminal in the project directory. tsc --target ES5 --experimentalDecorators
  • Or, you can update the tsconfig.js file and add "experimentalDecorators": true attribute in compilerOptions object.
    {
        "compilerOptions": {
            "target": "ES5",
            "experimentalDecorators": true
        }
    }
    

Decorator Syntax

You can follow the syntax below to use decorators in TypeScript.

@DecoratorName

In the above syntax, 'DecoratorName' is a function name that is prefixed with the '@' symbol. The expression must evaluate the 'DecorateName' function at the run time.

Decorator Factories

Whenever you need to customize how the decorator function is applied to declarations, they can use the decorator factories. A decorator factory function returns the expression which will be evaluated at the run time.

Follow the syntax below to use the decorator factory function.

function decoratorName(args: string) {
  // Decorator factory returns the function expression
  return function (target) {
    // This is the decorator which will be evaluated at the run time.
  };
}

In the above syntax, 'args' is an argument passed to the decorator function, and 'target' is a prototype of the class.

Decorator Composition

You can use multiple decorators with particular declarations. Multiple decorators either can be used in a single line or multiline as shown below.

In a single line:

@f @g x

OR, In multi lines:

@f
@g
x

In the above syntax, 'f' and 'g' decorators are used with a single declaration 'x'.

Why Use Decorators?

Let's take a simple example to understand the use cases of decorators.

// Defining a class
class Student {
    // Declaring the properties of the class
    constructor(private name: string, private rollNo: number) { }

    // Defining the methods of the class
    sayHello() {
        console.log(`Hello, my name is ${this.name}.`);
    }

    printrollNo() {
        console.log(`My RollNo is ${this.rollNo}.`);
    }
}

// Creating an object of the class
const user = new Student("John", 20);
// Accessing the properties of the class
user.sayHello();
user.printrollNo();

On compiling, it will generate the following JavaScript code:

// Defining a class
class Student {
    // Declaring the properties of the class
    constructor(name, rollNo) {
        this.name = name;
        this.rollNo = rollNo;
    }
    // Defining the methods of the class
    sayHello() {
        console.log(`Hello, my name is ${this.name}.`);
    }
    printrollNo() {
        console.log(`My RollNo is ${this.rollNo}.`);
    }
}

// Creating an object of the class
const user = new Student("John", 20);
// Accessing the properties of the class
user.sayHello();
user.printrollNo();

It produces the following output:

Hello, my name is John.
My RollNo is 20.

Now, what if we want to log the function when the execution of the function starts and ends? We need to add logs at the start and end of each function as shown in the below code.

// Defining a class
class Student {
    // Declaring the properties of the class
    constructor(private name: string, private rollNo: number) { }

    // Defining the methods of the class
    sayHello() {
        console.log("Start: sayHello");
        console.log(`Hello, my name is ${this.name}.`);
        console.log("End: sayHello");
    }

    printrollNo() {
        console.log("Start: printrollNo");
        console.log(`My RollNo is ${this.rollNo}.`);
        console.log("End: printrollNo");
    }
}

// Creating an object of the class
const user = new Student("John", 20);
// Accessing the properties of the class
user.sayHello();
user.printrollNo();

On compiling, it will generate the following JavaScript code:

// Defining a class
class Student {
    // Declaring the properties of the class
    constructor(name, rollNo) {
        this.name = name;
        this.rollNo = rollNo;
    }
    // Defining the methods of the class
    sayHello() {
        console.log("Start: sayHello");
        console.log(`Hello, my name is ${this.name}.`);
        console.log("End: sayHello");
    }
    printrollNo() {
        console.log("Start: printrollNo");
        console.log(`My RollNo is ${this.rollNo}.`);
        console.log("End: printrollNo");
    }
}
// Creating an object of the class
const user = new Student("John", 20);
// Accessing the properties of the class
user.sayHello();
user.printrollNo();

It will produce the following output:

Start: sayHello
Hello, my name is John.
End: sayHello
Start: printrollNo
My RollNo is 20.
End: printrollNo

What if we want to reuse the logic of logging the function execution without writing the repeated code? Here, decorators come into the picture.

Let's learn it via the example below.

// Decorator factory
function printExecution(method: any, _context: any) {
    // Returning a new function
    return function (value: any, ...args: any[]) {
        // Logging the method name at the start
        console.log("start:", method.name);
        // Calling the original method
        const result = method.call(value, ...args);
        // Logging the method name at the end
        console.log("end:", method.name);
        return result;
    }
}

// Defining a class
class Student {
    // Declaring the properties of the class
    constructor(private name: string, private rollNo: number) { }

    // Defining the methods of the class
    @printExecution
    sayHello() {
        console.log(`Hello, my name is ${this.name}.`);
    }

    @printExecution
    printrollNo() {
        console.log(`My RollNo is ${this.rollNo}.`);
    }
}

// Creating an object of the class
const user = new Student("John", 20);
// Accessing the properties of the class
user.sayHello();
user.printrollNo();

The above code prints the same output as the previous code.

Start: sayHello
Hello, my name is John.
End: sayHello
Start: printrollNo
My RollNo is 20.
End: printrollNo

Class Decorators

Class decorators are used with the class declaration to observe or modify the class definition.

Example

// Decorator factory
function LogClass(target: Function) {
    console.log(`${target.name} is instantiated`);
}

// Decorator
@LogClass
class MyClass {
    constructor() { console.log("MyClass instance created"); }
}

// Create an instance of the class
const myClassInstance = new MyClass();

On compilation, it will generate the following JavaScript code:

// Class definition
class MyClass {
    constructor() { console.log("MyClass instance created"); }
}
// Decorator
LogClass(MyClass);
// Create an instance of the class
const myClassInstance = new MyClass();

Output

MyClass instance created
MyClass is instantiated

Method Decorators

The method decorators are used to replace, modify, or observe the method definition. It is used with the method definition.

Example

// Decorator factory
function printExecution(method: any, _context: any) {
    // Returning a new function
    return function (value: any, ...args: any[]) {
        // Logging the method name at the start
        console.log("start:", method.name);
        // Calling the original method
        const result = method.call(value, ...args);
        return result;
    }
}

// Defining a class
class Person {
    constructor(private name: string) { }

    // Decorator
    @printExecution
    printName() {
        console.log(`Hello, my name is ${this.name}.`);
    }
}

// Create an object of the class
const user = new Person("John");
// Accessing the properties of the class
user.printName();

Output

start: printName
Hello, my name is John.

Accessor Decorators

Accessor decorators are used with the get() and set() accessors to observe, replace, and modify the definition of the accessor.

Example

// Decorator factory
function LogAccessor(method: any) {
    console.log("Getter called");
}

// Define a class
class MyClass {
    private _name: string = "MyClass";

    // Decorator
    @LogAccessor
    get name() {
        return this._name;
    }
}

const instance = new MyClass();
console.log(instance.name);

Output

Getter called
MyClass

Property Decorators

Property decorators are used with the property to modify, replace, and observe it.

Example

// Decorator function to log the property name
function LogProperty(target: any, propertyKey: string) {
    console.log(`Property declared: ${propertyKey}`);
}

class Person {
    // Decorator is applied to the property
    @LogProperty
    name: string;

    @LogProperty
    age: number;

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

let person = new Person('Jay', 23);

Output

Property declared: name
Property declared: age

Parameter Decorators

Parameter decorators are used with the method parameters to observe, modify, and replace them.

Example

// Decorator with parameter
function LogParameter(target: any, methodName: string, parameterIndex: number) {
    console.log(`Parameter in ${methodName} at index ${parameterIndex} has been decorated`);
}

// Class decorator
class MyClass {
    myMethod(@LogParameter param: string) {
        console.log(`Executing myMethod with param: ${param}`);
    }
}

// Create an instance of the class
const instance = new MyClass();
instance.myMethod("test");

Output

Parameter in myMethod at index 0 has been decorated
Executing myMethod with param: test

We have learned to create custom decorators in TypeScript. However, users can use the pre-defined decorators for various purposes like debugging the code, etc.

Advertisements