TypeScript Decorators: What

In the world of TypeScript, decorators are a powerful feature that allows you to add metadata and behavior to classes, methods, accessors, properties, or parameters at design time. They are a way to extend the functionality of existing code without modifying its core structure. Decorators are a form of metaprogramming, enabling developers to write more modular, reusable, and maintainable code. This blog post will explore the core concepts of TypeScript decorators, their typical usage scenarios, and common best practices.

Table of Contents

  1. Core Concepts of TypeScript Decorators
    • What are Decorators?
    • Decorator Factories
    • Decorator Composition
  2. Typical Usage Scenarios
    • Class Decorators
    • Method Decorators
    • Property Decorators
    • Parameter Decorators
  3. Common Best Practices
    • Using Decorators for Logging
    • Validation with Decorators
    • Dependency Injection with Decorators
  4. Conclusion
  5. FAQ
  6. References

Detailed and Structured Article

Core Concepts of TypeScript Decorators

What are Decorators?

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

Here is a simple example of a class decorator:

function logClass(constructor: Function) {
    console.log(`Class ${constructor.name} was created.`);
}

@logClass
class MyClass {
    constructor() {
        console.log('Inside MyClass constructor');
    }
}

const instance = new MyClass();

In this example, the logClass function is a decorator. It takes the constructor of the class as an argument and logs a message when the class is defined.

Decorator Factories

A decorator factory is a function that returns a decorator. This allows you to pass arguments to the decorator.

function logClassWithMessage(message: string) {
    return function(constructor: Function) {
        console.log(`${message}: ${constructor.name}`);
    };
}

@logClassWithMessage('Custom message for')
class AnotherClass {
    constructor() {
        console.log('Inside AnotherClass constructor');
    }
}

Here, logClassWithMessage is a decorator factory. It takes a message parameter and returns a decorator function.

Decorator Composition

Multiple decorators can be applied to a single declaration. The order of execution of decorators is important. Decorators are evaluated from bottom to top, but applied from top to bottom.

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

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

class MyClassWithMultipleDecorators {
    @first()
    @second()
    method() {}
}

Typical Usage Scenarios

Class Decorators

Class decorators are applied to the constructor of a class. They can be used to modify the constructor or add new functionality to the class.

function addStaticProperty(constructor: Function) {
    constructor['staticProperty'] = 'This is a static property';
}

@addStaticProperty
class MyClassWithStaticProperty {
    constructor() {}
}

console.log(MyClassWithStaticProperty['staticProperty']);

Method Decorators

Method decorators are applied to a method of a class. They can be used to modify the behavior of the method, such as adding logging or validation.

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

class MyClassWithMethodDecorator {
    @logMethod
    add(a: number, b: number) {
        return a + b;
    }
}

const instanceWithMethodDecorator = new MyClassWithMethodDecorator();
instanceWithMethodDecorator.add(1, 2);

Property Decorators

Property decorators are applied to a property of a class. They can be used to add metadata or modify the behavior of property access.

function readOnly(target: any, propertyKey: string) {
    let descriptor: PropertyDescriptor = {
        writable: false
    };
    Object.defineProperty(target, propertyKey, descriptor);
}

class MyClassWithPropertyDecorator {
    @readOnly
    public myProperty = 'Initial value';
}

const instanceWithPropertyDecorator = new MyClassWithPropertyDecorator();
// This will throw an error because the property is read-only
// instanceWithPropertyDecorator.myProperty = 'New value';

Parameter Decorators

Parameter decorators are applied to a parameter of a method. They can be used to add metadata about the parameter.

function logParameter(target: any, propertyKey: string, parameterIndex: number) {
    console.log(`Parameter at index ${parameterIndex} of method ${propertyKey} was logged.`);
}

class MyClassWithParameterDecorator {
    method(@logParameter param: string) {}
}

Common Best Practices

Using Decorators for Logging

As shown in the method decorator example, decorators can be used to add logging to methods. This can be useful for debugging and monitoring the application.

Validation with Decorators

Decorators can be used to add validation logic to methods or properties. For example, a decorator can be used to validate that a parameter is a valid email address.

function validateEmail(target: any, propertyKey: string, parameterIndex: number) {
    const originalMethod = target[propertyKey];
    target[propertyKey] = function(...args: any[]) {
        const email = args[parameterIndex];
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        if (!emailRegex.test(email)) {
            throw new Error('Invalid email address');
        }
        return originalMethod.apply(this, args);
    };
}

class UserService {
    createUser(@validateEmail email: string) {
        console.log(`Creating user with email: ${email}`);
    }
}

const userService = new UserService();
// This will throw an error
// userService.createUser('invalidemail');
userService.createUser('[email protected]');

Dependency Injection with Decorators

Decorators can be used to implement dependency injection. For example, a decorator can be used to mark a property as a dependency that needs to be injected.

function Injectable(target: any) {
    target['isInjectable'] = true;
}

@Injectable
class DatabaseService {
    constructor() {}
}

function Inject(target: any, propertyKey: string) {
    const dependency = new (target.constructor[propertyKey])();
    target[propertyKey] = dependency;
}

class UserRepository {
    @Inject
    databaseService: DatabaseService;

    constructor() {}

    getData() {
        console.log('Getting data from database service');
    }
}

const userRepository = new UserRepository();
userRepository.getData();

Conclusion

TypeScript decorators are a powerful feature that allows developers to add metadata and behavior to classes, methods, properties, and parameters. They provide a way to write more modular and reusable code. By understanding the core concepts, typical usage scenarios, and best practices, developers can leverage decorators to improve the quality and maintainability of their TypeScript applications.

FAQ

Q1: Are decorators a part of the ECMAScript standard?

A1: As of now, decorators are a stage 2 proposal in the ECMAScript standard. TypeScript has implemented them as an experimental feature.

Q2: Can I use decorators in JavaScript?

A2: Decorators are not natively supported in JavaScript. However, you can use TypeScript to write decorators and then transpile the code to JavaScript.

Q3: What is the difference between a decorator and a decorator factory?

A3: A decorator is a function that is directly applied to a declaration. A decorator factory is a function that returns a decorator, allowing you to pass arguments to the decorator.

References