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.