Wrapping Methods with Decorators in TypeScript

Updated: January 8, 2024 By: Guest Contributor Post a comment

Overview

This tutorial explores the utility and implementation of decorators in TypeScript, a feature that provides a way to add both annotations and meta-programming syntax for class declarations and members.

Decorators are a stage 2 proposal for JavaScript and are available as an experimental feature of TypeScript. They are a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Decorators use the form @expression, where expression must evaluate to a function that will be called at runtime with information about the decorated declaration.

In TypeScript, decorators offer a way to observe, modify, or replace a class declaration or method definition. To enable decorator support in TypeScript, you must enable the experimentalDecorators compiler option either on the command line or in your tsconfig.json file.

Basic Decorator Usage

A basic method decorator in TypeScript can log the call of the method it decorates. Here is how you can create a simple logging decorator:

function Log(target: any, propertyName: string, propertyDesciptor: PropertyDescriptor): PropertyDescriptor {
    const method = propertyDesciptor.value;
    propertyDesciptor.value = function (...args: any[]) {
        console.log(`Calling ${propertyName} with args: ${args}`);
        return method.apply(this, args);
    };
    return propertyDesciptor;
}

class Person {
    @Log
    say(message: string) {
        console.log(message);
    }
}

const newPerson = new Person();
newPerson.say('Hello, World!');

This decorator logs the method call and its arguments every time the say method is called. The @Log decorator is a method decorator that will apply to the say method of the Person class.

Timing Decorator

Here’s an advanced example of a method decorator that measures the execution time of a method:

function Measure(target: any, propertyName: string, propertyDescriptor: PropertyDescriptor): PropertyDescriptor {
    const method = propertyDescriptor.value;
    propertyDescriptor.value = function (...args: any[]) {
        const start = performance.now();
        const result = method.apply(this, args);
        const end = performance.now();
        console.log(`${propertyName} took ${end - start} milliseconds`);
        return result;
    };
    return propertyDescriptor;
}

class Task {
    @Measure
    doWork() {
        // some work is done here
    }
}

When doWork is called, the decorator will print out the time it took for the method to execute.

Access Control Decorator

Decorators can also be used for access control. Below is an example of a method decorator that allows method execution only if a user has the appropriate role:

function Authorize(roles: string[]) {
    return function (target: any, propertyName: string, propertyDescriptor: PropertyDescriptor): PropertyDescriptor {
        const method = propertyDescriptor.value;
        propertyDescriptor.value = function (...args: any[]) {
            const user = // logic to determine current user's role
            if (roles.includes(user.role)) {
                return method.apply(this, args);
            } else {
                throw new Error('Unauthorized');
            }
        };
        return propertyDescriptor;
    };
}

class AdminActions {
    @Authorize(['admin'])
    deleteAccount() {
       console.log('Account deleted successfully.');
    }
}

The deleteAccount method will only execute if the current user is an admin.

Auto-Bind Decorator

Sometimes it’s necessary to automatically bind a method to the instance of the class. The auto-bind decorator does exactly this:

function AutoBind(target: any, methodName: string, descriptor: PropertyDescriptor): PropertyDescriptor {
    const originalMethod = descriptor.value;
    return {
       configurable: true,
       get() {
           return originalMethod.bind(this);
       }
    };
}

class Component {
    state: string = 'active';

    @AutoBind
    handleEvent() {
        console.log(this.state);
    }
}

const button = document.querySelector('button')!;
button.addEventListener('click', new Component().handleEvent);

With the AutoBind decorator applied, handleEvent will always be called with Component as this, even if it is triggered by a DOM event.

Parameter Decorators

Decorators can also be applied to parameters. A parameter decorator can log the parameter value every time a method is called:

function LogParameter(target: any, methodName: string, parameterIndex: number) {
    const key = `${methodName}_decorated_params`;
    if (Array.isArray(target[key])) {
        target[key].push(parameterIndex);
    } else {
        target[key] = [parameterIndex];
    }
}

class Calculator {
    compute(@LogParameter arg1: number, @LogParameter arg2: number): number {
        console.log(`Arguments: ${arg1}, ${arg2}`);
        return arg1 + arg2;
    }
}

const calculator = new Calculator();
calculator.compute(25, 75);

In this example, the arguments to the compute method are logged thanks to the @LogParameter decorators.

Conclusion

Decorators in TypeScript provide powerful and expressive tools for writing cleaner, more declarative and maintainable code. With decorators, you can easily extend the functionality of methods, accessors, properties, and parameters without sacrificing the readability of the code.