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.
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.
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
}
}
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}`);
}
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.
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());
sealed
decorator seals the constructor and its prototype, preventing new properties from being added to them.@sealed
decorator is applied to the Greeter
class.Greeter
class and its instances cannot have new properties added after creation.
Hello, world
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.
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));
enumerable
decorator factory takes a boolean value and returns a method decorator.@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
John Doe
[]
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.
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);
configurable
decorator factory takes a boolean value and returns an accessor decorator.@configurable(false)
decorator is applied to the make
getter, making it non-configurable.make
property cannot be reconfigured.
Toyota
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.
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);
format
decorator factory takes a format string and returns a property decorator.@format('Mr./Ms.')
decorator is applied to the name
property.name
property’s setter appends the format string to the new value.
Mr./Ms. John Doe
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.
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));
logParameter
decorator logs the parameter index.@logParameter
decorator is applied to the parameters of the add
method.Calculator
class.
5
Decorator factories are functions that return decorators. They allow you to pass parameters to decorators.
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);
logMethod
decorator factory takes a string parameter and returns a method decorator.@logMethod('Adding numbers')
decorator is applied to the add
method.
Adding numbers - Arguments: 2, 3
Adding numbers - Result: 5
Multiple decorators can be applied to a single target, and they are executed in the order they are declared.
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();
first
and second
, are defined.method
in the Example
class.
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 !❤️