Welcome back to our TypeScript series!
Today, we’ll explore decorators, a powerful feature that allows you to modify classes, methods, properties, and parameters. Decorators provide a way to add annotations and a meta-programming syntax for class declarations and members. They are widely used in frameworks like Angular for dependency injection, routing, and more.
What are Decorators?
Decorators are special functions that can be attached to classes, methods, accessors, properties, or parameters. They provide a way to add behavior to these elements in a declarative way. In TypeScript, decorators are an experimental feature and require the experimentalDecorators
compiler option to be enabled in tsconfig.json
.
Enabling Decorators
To use decorators, you need to enable them in your tsconfig.json
file:
{
"compilerOptions": {
"experimentalDecorators": true
}
}
Types of Decorators
There are several types of decorators in TypeScript:
Class Decorators
Method Decorators
Accessor Decorators
Property Decorators
Parameter Decorators
1. Class Decorators
A class decorator is a function that takes a class constructor as an argument and can modify or replace the class.
Example: 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}`;
}
}
In this example, the sealed
decorator seals the Greeter
class, preventing new properties from being added to it.
2. Method Decorators
A method decorator is a function that takes three arguments: the target (class prototype), the property key, and the property descriptor.
It can modify the method or its descriptor.
Example: Method Decorator
function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false)
greet() {
return `Hello, ${this.greeting}`;
}
}
Here, the enumerable
decorator sets the enumerable
property of the greet
method descriptor to false
.
3. Accessor Decorators
An accessor decorator is similar to a method decorator but is applied to getters and setters. It takes the same arguments as a method decorator.
Example: Accessor Decorator
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@configurable(false)
get x() {
return this._x;
}
@configurable(false)
get y() {
return this._y;
}
}
In this example, the configurable
decorator sets the configurable
property of the accessor descriptors to false
.
4. Property Decorators
A property decorator is a function that takes two arguments: the target (class prototype) and the property key. It can modify the property but does not have access to the property descriptor.
Example: Property Decorator
function logProperty(target: any, key: string) {
let _val = target[key];
const getter = () => {
console.log(`Get: ${key} => ${_val}`);
return _val;
};
const setter = (newVal) => {
console.log(`Set: ${key} => ${newVal}`);
_val = newVal;
};
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
}
class Person {
@logProperty
name: string;
constructor(name: string) {
this.name = name;
}
}
let p = new Person("Alice");
p.name = "Bob"; // Output: Set: name => Bob
console.log(p.name); // Output: Get: name => Bob
Here, the logProperty
decorator logs get and set operations on the name
property.
5. Parameter Decorators
A parameter decorator is a function that takes three arguments: the target (class prototype), the property key, and the parameter index. It is used to add metadata about the parameters.
Example: 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 Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet(@logParameter name: string) {
return `Hello, ${name}! ${this.greeting}`;
}
}
In this example, the logParameter
decorator logs the parameter index of the greet
method.
Combining Decorators
You can combine multiple decorators by stacking them, applying each decorator from top to bottom.
Example: Combining Decorators
function first() {
console.log('first(): factory evaluated');
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log('first(): called');
};
}
function second() {
console.log('second(): factory evaluated');
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log('second(): called');
};
}
class ExampleClass {
@first()
@second()
method() {}
}
The output will be:
first(): factory evaluated
second(): factory evaluated
second(): called
first(): called
Practical Use Cases
Decorators are especially useful in frameworks and libraries for:
Metadata: Adding metadata to classes and members.
Dependency Injection: Annotating services and dependencies.
Validation: Adding validation rules to properties and methods.
Logging: Logging method calls and property access.
Summary
Today, we explored decorators in TypeScript, covering class decorators, method decorators, accessor decorators, property decorators, and parameter decorators. Decorators provide a powerful way to add behavior to your code in a declarative manner. Understanding and using decorators effectively can help you write more maintainable and expressive code.
Next time, we’ll dive into the TypeScript Compiler API. Stay tuned!