TypeScript Decorators

Decorators in TypeScript provide a way to add metadata or modify the behavior of classes, methods, properties, or parameters. They are a powerful feature that allows you to extend and customize your code in a clean and maintainable way. In this chapter, we will explore the concept of decorators in TypeScript from basic to advanced levels, providing comprehensive explanations, code examples, and detailed explanations of each example.

Understanding Decorators

What are Decorators?

Decorators are special declarations that can be attached to a class, method, accessor, property, or parameter. They provide a way to add annotations and a meta-programming syntax for class declarations and members. Decorators are functions that are invoked with specific arguments depending on their declaration context.

Enabling Decorators

Before using decorators, you need to enable the experimental decorator support in TypeScript by adding the following to your tsconfig.json:

				
					{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
				
			

Basic Syntax of a Decorator

A decorator is a function that takes a target, property key, and descriptor as arguments. Here is a basic example of a decorator function:

				
					function simpleDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log(`Target: ${target}`);
    console.log(`PropertyKey: ${propertyKey}`);
    console.log(`Descriptor: ${descriptor}`);
}
				
			

Class Decorators

What is a Class Decorator?

A class decorator is a function that is applied to the constructor of a class. It can be used to observe, modify, or replace a class definition.

Example of a Class Decorator

				
					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}`;
    }
}

const greeter = new Greeter('world');
console.log(greeter.greet());
				
			

Explanation:

  • The sealed decorator seals the constructor and its prototype, preventing new properties from being added to them.
  • The @sealed decorator is applied to the Greeter class.
  • The Greeter class and its instances cannot have new properties added after creation.

Output:

				
					Hello, world
				
			

Method Decorators

What is a Method Decorator?

A method decorator is a function that is applied to the property descriptor of a method. It can be used to modify the behavior of the method.

Example of a Method Decorator

				
					function enumerable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.enumerable = value;
    };
}

class Person {
    constructor(public firstName: string, public lastName: string) {}
    
    @enumerable(false)
    getFullName() {
        return `${this.firstName} ${this.lastName}`;
    }
}

const person = new Person('John', 'Doe');
console.log(person.getFullName());
console.log(Object.keys(person));

				
			

Explanation:

  • The enumerable decorator factory takes a boolean value and returns a method decorator.
  • The @enumerable(false) decorator is applied to the getFullName method, making it non-enumerable.
  • Object.keys(person) does not include getFullName because it is non-enumerable

Output:

				
					John Doe
[]
				
			

Accessor Decorators

What is an Accessor Decorator?

An accessor decorator is applied to the property descriptor for an accessor (getter or setter). It can be used to observe, modify, or replace an accessor.

Example of an Accessor Decorator

				
					function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
    };
}

class Car {
    private _make: string;
    
    constructor(make: string) {
        this._make = make;
    }

    @configurable(false)
    get make() {
        return this._make;
    }
}

const car = new Car('Toyota');
console.log(car.make);
				
			

Explanation:

  • The configurable decorator factory takes a boolean value and returns an accessor decorator.
  • The @configurable(false) decorator is applied to the make getter, making it non-configurable.
  • The make property cannot be reconfigured.

Output:

				
					Toyota
				
			

Property Decorators

What is a Property Decorator?

A property decorator is a function that is applied to a property in a class. It cannot directly modify the property’s value, but it can be used to observe or modify the property’s metadata.

Example of a Property Decorator

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

        const getter = () => value;
        const setter = (newVal: string) => {
            value = `${formatString} ${newVal}`;
        };

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

class User {
    @format('Mr./Ms.')
    name: string;

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

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

Explanation:

  • The format decorator factory takes a format string and returns a property decorator.
  • The @format('Mr./Ms.') decorator is applied to the name property.
  • The name property’s setter appends the format string to the new value.

Output:

				
					Mr./Ms. John Doe
				
			

Parameter Decorators

What is a Parameter Decorator?

A parameter decorator is a function that is applied to the parameters of a method. It can be used to observe or modify the parameter’s metadata.

Example of a Parameter Decorator

				
					function logParameter(target: any, propertyKey: string, parameterIndex: number) {
    const metadataKey = `log_${propertyKey}_parameters`;

    if (Array.isArray(target[metadataKey])) {
        target[metadataKey].push(parameterIndex);
    } else {
        target[metadataKey] = [parameterIndex];
    }
}

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

const calculator = new Calculator();
console.log(calculator.add(2, 3));
				
			

Explanation:

  • The logParameter decorator logs the parameter index.
  • The @logParameter decorator is applied to the parameters of the add method.
  • The parameter indices are stored in metadata on the Calculator class.

Output:

				
					5
				
			

Advanced Use Cases of Decorators

Decorator Factories

Decorator factories are functions that return decorators. They allow you to pass parameters to decorators.

Example

				
					function logMethod(params: string) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;

        descriptor.value = function (...args: any[]) {
            console.log(`${params} - Arguments: ${args.join(', ')}`);
            const result = originalMethod.apply(this, args);
            console.log(`${params} - Result: ${result}`);
            return result;
        };

        return descriptor;
    };
}

class MathOperations {
    @logMethod('Adding numbers')
    add(a: number, b: number): number {
        return a + b;
    }
}

const math = new MathOperations();
math.add(2, 3);
				
			

Explanation:

  • The logMethod decorator factory takes a string parameter and returns a method decorator.
  • The decorator logs the method arguments and result.
  • The @logMethod('Adding numbers') decorator is applied to the add method.

Output:

				
					Adding numbers - Arguments: 2, 3
Adding numbers - Result: 5
				
			

Combining Multiple Decorators

Multiple decorators can be applied to a single target, and they are executed in the order they are declared.

Example

				
					function first() {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log('First decorator');
    };
}

function second() {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log('Second decorator');
    };
}

class Example {
    @first()
    @second()
    method() {
        console.log('Method executed');
    }
}

const example = new Example();
example.method();
				
			

Explanation:

  • Two decorators, first and second, are defined.
  • Both decorators are applied to the method in the Example class.
  • Decorators are executed in the order they are declared (bottom to top).

Output:

				
					Second decorator
First decorator
Method executed
				
			

TypeScript decorators provide a powerful and flexible way to extend and modify the behavior of classes, methods, properties, and parameters. This chapter covered the basics of decorators, including class, method, accessor, property, and parameter decorators, as well as advanced use cases such as decorator factories and combining multiple decorators. Happy coding !❤️

Table of Contents