Decorator in Typescript
A decorator in TypeScript is a special kind of declaration that can be attached to a class, method, accessor, property, or parameter. Decorators allow you to modify or enhance the behavior of the target they are applied to. They are widely used in frameworks like Angular for dependency injection, metadata, and more.
TypeScript decorators are an extension that allows adding annotation and metaprogramming to class declarations and their members in TypeScript. TypeScript supports decorators syntax as an experimental feature which is distinct from JavaScript decorators that is currently a Stage 3 ECMAScript proposal.
There are six stages in the ECMAScript proposal process: Stage 0 (Strawman), Stage 1, Stage 2, Stage 3, and Stage 4. A proposal must progress through all these stages, and TC39 (the technical committee) must approve its movement to the next stage.
Meta programming in programming languages refers to writing code that manipulates or generates other code. Decorators in Typescript are a specific example of metaprogramming, allowing you to modify the behavior of functions or classes without directly changing their code. They are used to add functionality to existing objects, much like the Decorator design pattern.
Decorators
Syntax
Decorators are prefixed with the @ symbol and are functions that are executed at runtime.
@decorator
class MyClass {
@decorator
myMethod() {}
}
Types of Decorators
-
Class Decorators
- Applied to a class.
- Used to modify or annotate the class.
function ClassDecorator(constructor: Function) {
console.log("ClassDecorator called");
}
@ClassDecorator
class MyClass {}Output: Logs
"ClassDecorator called"when the class is defined.
-
Method Decorators
- Applied to a method.
- Used to modify or observe the behavior of a method.
function MethodDecorator(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
console.log(`MethodDecorator called on: ${propertyKey}`);
}
class MyClass {
@MethodDecorator
myMethod() {
console.log("Executing myMethod");
}
}Output: Logs
"MethodDecorator called on: myMethod"when the class is defined.
-
Property Decorators
- Applied to a property.
- Used to observe or modify the behavior of a property.
function PropertyDecorator(target: Object, propertyKey: string) {
console.log(`PropertyDecorator called on: ${propertyKey}`);
}
class MyClass {
@PropertyDecorator
myProperty: string = "Hello";
}Output: Logs
"PropertyDecorator called on: myProperty"when the class is defined.
-
Accessor Decorators
- Applied to a getter or setter.
- Used to modify or observe the behavior of an accessor.
function AccessorDecorator(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
console.log(`AccessorDecorator called on: ${propertyKey}`);
}
class MyClass {
private _value: number = 0;
@AccessorDecorator
get value() {
return this._value;
}
set value(val: number) {
this._value = val;
}
}
-
Parameter Decorators
- Applied to a method parameter.
- Used to observe or modify metadata about a parameter.
function ParameterDecorator(target: Object, propertyKey: string, parameterIndex: number) {
console.log(`ParameterDecorator called on: ${propertyKey}, parameter index: ${parameterIndex}`);
}
class MyClass {
myMethod(@ParameterDecorator param: string) {
console.log(param);
}
}Output: Logs
"ParameterDecorator called on: myMethod, parameter index: 0"when the class is defined.
Practical Example: Logging Decorator
function Log(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Method ${propertyKey} called with arguments: ${args}`);
return originalMethod.apply(this, args);
};
return descriptor;
}
class Calculator {
@Log
add(a: number, b: number): number {
return a + b;
}
}
const calc = new Calculator();
console.log(calc.add(2, 3)); // Logs: Method add called with arguments: 2,3
// Output: 5
Key Points
-
Metadata Reflection:
- TypeScript decorators often work with metadata.
- You can use the
reflect-metadatalibrary to store and retrieve metadata.
import "reflect-metadata";
function MetadataDecorator(target: Object, propertyKey: string) {
Reflect.defineMetadata("custom:metadata", "value", target, propertyKey);
}
class MyClass {
@MetadataDecorator
myProperty: string;
}
console.log(Reflect.getMetadata("custom:metadata", MyClass.prototype, "myProperty")); // Output: "value" -
Order of Execution:
- Decorators are applied in the following order:
- Parameter Decorators
- Method/Accessor Decorators
- Property Decorators
- Class Decorators
- Decorators are applied in the following order:
-
Experimental Feature:
- Decorators are an experimental feature in TypeScript and require enabling the
experimentalDecoratorscompiler option in tsconfig.json:{
"compilerOptions": {
"experimentalDecorators": true
}
}
- Decorators are an experimental feature in TypeScript and require enabling the
Decorators are a powerful feature for adding metadata, modifying behavior, and implementing cross-cutting concerns like logging, validation, and dependency injection.
Factory Function in a Decorator
A factory function allows you to pass parameters to a decorator. It returns the actual decorator function.
Example: Logging Decorator with a Factory Function
function Log(message: string) {
// This is the factory function
return function (target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`${message} - Method ${propertyKey} called with arguments: ${args}`);
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class Calculator {
@Log("Addition Operation")
add(a: number, b: number): number {
return a + b;
}
@Log("Multiplication Operation")
multiply(a: number, b: number): number {
return a * b;
}
}
// Example usage
const calc = new Calculator();
console.log(calc.add(2, 3)); // Logs: "Addition Operation - Method add called with arguments: 2,3"
// Output: 5
console.log(calc.multiply(4, 5)); // Logs: "Multiplication Operation - Method multiply called with arguments: 4,5"
// Output: 20
Explanation
-
Factory Function:
- The
Logfunction is a factory function that takes amessageparameter. - It returns the actual decorator function.
- The
-
Decorator Function:
- The returned function is the actual decorator that modifies the method.
- It wraps the original method and adds logging functionality.
-
How It Works:
- When the
addormultiplymethod is called, the decorator logs the custom message and the method arguments before executing the original method.
- When the
Key Points
- Factory functions allow decorators to be dynamic and configurable.
- You can pass parameters to the factory function to customize the behavior of the decorator.
- This approach is commonly used in frameworks like Angular for dependency injection and metadata configuration.
difference between experimental and ECMAScript decorators
The difference between experimental decorators and ECMAScript decorators in TypeScript lies in their implementation, syntax, and alignment with the ECMAScript standard. Here's a detailed comparison:
1. Experimental Decorators
- Introduced: TypeScript's experimental decorators were introduced before the ECMAScript proposal for decorators was finalized.
- Status: Non-standard and based on an earlier stage of the ECMAScript proposal.
- Syntax: Uses the
@decoratorsyntax. - Behavior:
- Allows decorating classes, methods, properties, accessors, and parameters.
- Requires enabling the
experimentalDecoratorscompiler option in tsconfig.json.
Example:
function Log(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Method ${propertyKey} called with arguments: ${args}`);
return originalMethod.apply(this, args);
};
return descriptor;
}
class Calculator {
@Log
add(a: number, b: number): number {
return a + b;
}
}
Key Points:
- Metadata Reflection: Often used with the
reflect-metadatalibrary for storing and retrieving metadata. - Order of Execution: Follows a specific order (parameter → method/accessor → property → class).
- Compiler Option: Requires
"experimentalDecorators": truein tsconfig.json.
2. ECMAScript Decorators
- Introduced: Based on the finalized ECMAScript proposal for decorators (Stage 3+).
- Status: Standardized and aligned with the ECMAScript specification.
- Syntax: Uses the
@decoratorsyntax but with stricter rules and a different API. - Behavior:
- Focuses on class elements (methods, fields, accessors).
- Does not support parameter decorators.
- Requires enabling the
useDefineForClassFieldscompiler option in tsconfig.json.
Example:
function Log(value: any, context: ClassMethodDecoratorContext) {
const originalMethod = value;
return function (...args: any[]) {
console.log(`Method ${context.name} called with arguments: ${args}`);
return originalMethod.apply(this, args);
};
}
class Calculator {
@Log
add(a: number, b: number): number {
return a + b;
}
}
Key Points:
- Metadata Reflection: Does not rely on
reflect-metadataby default. - Context Object: Provides a
contextobject with metadata about the decorated element (e.g.,name,kind). - Compiler Option: Requires
"experimentalDecorators": trueand"useDefineForClassFields": truein tsconfig.json.
Key Differences
| Feature | Experimental Decorators | ECMAScript Decorators |
|---|---|---|
| Standardization | Non-standard (TypeScript-specific) | Standardized (aligned with ECMAScript) |
| Metadata Reflection | Requires reflect-metadata | Does not rely on reflect-metadata |
| Supported Targets | Classes, methods, properties, accessors, parameters | Class elements (methods, fields, accessors) |
| Parameter Decorators | Supported | Not supported |
| Context Object | Not available | Provides context object |
| Compiler Options | "experimentalDecorators": true | "experimentalDecorators": true and "useDefineForClassFields": true |
| Order of Execution | Parameter → Method/Accessor → Property → Class | Class elements only |
Which One Should You Use?
-
Experimental Decorators:
- Use if you're working with older TypeScript projects or frameworks like Angular that rely on the experimental decorator implementation.
- Requires
reflect-metadatafor advanced use cases.
-
ECMAScript Decorators:
- Use if you're targeting modern JavaScript and want to align with the ECMAScript standard.
- Provides a cleaner and more standardized API.
Conclusion
- Experimental decorators are TypeScript's legacy implementation, while ECMAScript decorators are the standardized version.
- As ECMAScript decorators become widely adopted, they are expected to replace experimental decorators in the long term.