Table of Contents
- What Are Decorators?
- Setting Up TypeScript for Decorators
- Types of Decorators
- Decorator Factories
- Order of Execution
- Practical Use Cases
- Advanced Concepts: Metadata Reflection
- Limitations and Considerations
- Conclusion
- References
1. What Are Decorators?
At their core, decorators are functions that “wrap” or modify the target (class, method, etc.) they are applied to. They execute when the target is defined (not when instances are created), making them ideal for design-time modifications.
Decorators use the @decoratorName syntax, placed directly above the declaration they modify. For example:
@logClass
class User { ... }
Here, logClass is a decorator function that modifies the User class.
2. Setting Up TypeScript for Decorators
Decorators are not enabled by default in TypeScript. To use them, configure your tsconfig.json:
Step 1: Enable Experimental Features
Add these settings to tsconfig.json:
{
"compilerOptions": {
"target": "ES5" or higher, // Decorators require ES5+
"experimentalDecorators": true, // Enable decorators
"emitDecoratorMetadata": true // Optional: Emit metadata for reflection
}
}
emitDecoratorMetadata: Enables TypeScript to emit type metadata (e.g., parameter types) using thereflect-metadatalibrary (covered later).
3. Types of Decorators
TypeScript supports five types of decorators, each with a specific syntax and use case.
3.1 Class Decorators
Applied to classes. They take the class constructor as their only argument and can return a new constructor to replace the original class.
Syntax:
function decoratorName(constructor: Function) {
// Modify the constructor or its prototype
}
@decoratorName
class MyClass { ... }
Example: Add a Static Property
function addVersion(version: string) {
return function (constructor: Function) {
constructor.prototype.version = version; // Add instance property
constructor.version = version; // Add static property
};
}
@addVersion("1.0.0")
class App {
static version: string; // Static property added by decorator
version: string; // Instance property added by decorator
}
console.log(App.version); // "1.0.0" (static)
const app = new App();
console.log(app.version); // "1.0.0" (instance)
3.2 Method Decorators
Applied to class methods (including static methods). They intercept method calls and can modify the method’s behavior (e.g., add logging, validation).
Syntax:
function decoratorName(
target: Object, // Prototype of the class (or constructor for static methods)
propertyKey: string | symbol, // Name of the method
descriptor: PropertyDescriptor // Descriptor of the method (e.g., value, writable)
) {
// Modify the descriptor to wrap the method
}
class MyClass {
@decoratorName
myMethod() { ... }
}
Example: Log Method Calls
function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value; // Store the original method
// Replace the method with a wrapped version
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with args:`, args);
const result = originalMethod.apply(this, args); // Call original method
console.log(`${propertyKey} returned:`, result);
return result;
};
return descriptor;
}
class Calculator {
@logMethod
add(a: number, b: number): number {
return a + b;
}
}
const calc = new Calculator();
calc.add(2, 3);
// Output:
// Calling add with args: [2, 3]
// add returned: 5
3.3 Accessor Decorators
Applied to getters or setters (accessors). Similar to method decorators but target accessor descriptors.
Syntax:
function decoratorName(
target: Object,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) {
// Modify the accessor descriptor
}
class MyClass {
private _value: string;
@decoratorName
get value() { return this._value; }
@decoratorName
set value(newValue: string) { this._value = newValue; }
}
Example: Validate Setter Input
function validateString(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalSetter = descriptor.set;
descriptor.set = function (value: string) {
if (typeof value !== "string") {
throw new Error(`Invalid value for ${propertyKey}: must be a string`);
}
originalSetter?.call(this, value); // Call original setter
};
}
class User {
private _name: string;
@validateString
set name(value: string) {
this._name = value;
}
get name() {
return this._name;
}
}
const user = new User();
user.name = "Alice"; // Valid
user.name = 123; // Throws: "Invalid value for name: must be a string"
3.4 Property Decorators
Applied to class properties (instance or static). They cannot directly modify the property (TypeScript doesn’t expose the property descriptor), but they can use metadata to track properties.
Syntax:
function decoratorName(target: Object, propertyKey: string | symbol) {
// Use metadata to track the property
}
class MyClass {
@decoratorName
myProperty: string;
}
Example: Track Property Names
import "reflect-metadata"; // Required for metadata
const PROPERTY_KEYS = "property:keys";
function trackProperty(target: any, propertyKey: string) {
const existingKeys = Reflect.getMetadata(PROPERTY_KEYS, target) || [];
Reflect.defineMetadata(PROPERTY_KEYS, [...existingKeys, propertyKey], target);
}
class Product {
@trackProperty
name: string;
@trackProperty
price: number;
static getTrackedProperties() {
return Reflect.getMetadata(PROPERTY_KEYS, Product.prototype);
}
}
console.log(Product.getTrackedProperties()); // ["name", "price"]
3.5 Parameter Decorators
Applied to method parameters. They track parameter metadata (e.g., parameter index, custom labels) and are often used with dependency injection.
Syntax:
function decoratorName(
target: Object, // Prototype (or constructor for static methods)
propertyKey: string | symbol, // Method name
parameterIndex: number // Index of the parameter
) {
// Track parameter metadata
}
class MyClass {
myMethod(@decoratorName param1: string) { ... }
}
Example: Mark Required Parameters
import "reflect-metadata";
const REQUIRED_PARAMS = "params:required";
function required(target: any, propertyKey: string, parameterIndex: number) {
const existingParams = Reflect.getMetadata(REQUIRED_PARAMS, target, propertyKey) || [];
Reflect.defineMetadata(REQUIRED_PARAMS, [...existingParams, parameterIndex], target, propertyKey);
}
function validateParams(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const requiredParams = Reflect.getMetadata(REQUIRED_PARAMS, target, propertyKey) || [];
requiredParams.forEach((index: number) => {
if (args[index] === undefined || args[index] === null) {
throw new Error(`Parameter at index ${index} is required`);
}
});
return originalMethod.apply(this, args);
};
}
class UserService {
@validateParams
createUser(@required name: string, age?: number) {
return { name, age };
}
}
const service = new UserService();
service.createUser("Alice"); // Valid (age is optional)
service.createUser(null); // Throws: "Parameter at index 0 is required"
4. Decorator Factories
Decorators are often used with factories—functions that return a decorator. Factories let you pass arguments to customize the decorator’s behavior.
Example: Log with Custom Message
function log(message: string) {
// Factory returns a method decorator
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[${message}] Calling ${propertyKey}`);
return originalMethod.apply(this, args);
};
};
}
class DataService {
@log("API Call")
fetchData() {
return "Data from API";
}
}
const service = new DataService();
service.fetchData(); // Logs: "[API Call] Calling fetchData"
5. Order of Execution
Decorators execute when the class is defined (not when instances are created). Their execution order follows strict rules:
For a Single Declaration:
If multiple decorators are applied to the same target (e.g., a method), they run in reverse order of declaration.
function first() { console.log("First decorator"); return () => {}; }
function second() { console.log("Second decorator"); return () => {}; }
class Example {
@first()
@second()
method() {}
}
// Output:
// Second decorator (factory runs first)
// First decorator (factory runs next)
For a Class with Multiple Decorators:
Execution order:
- Parameter decorators (per method).
- Method/accessor decorators.
- Property decorators.
- Class decorators.
6. Practical Use Cases
Decorators shine for cross-cutting concerns. Here are real-world examples:
6.1 Caching
Cache method results to avoid redundant computations:
function cache(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const cache = new Map<string, any>();
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = originalMethod.apply(this, args);
cache.set(key, result);
return result;
};
}
class MathUtils {
@cache
factorial(n: number): number {
if (n <= 1) return 1;
return n * this.factorial(n - 1);
}
}
const utils = new MathUtils();
utils.factorial(5); // Computes and caches
utils.factorial(5); // Returns cached result
6.2 Authorization
Block method execution if the user lacks permissions:
function authorize(role: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const userRole = this.userRole; // Assume `userRole` is defined on the instance
if (userRole !== role) {
throw new Error("Unauthorized: Insufficient permissions");
}
return originalMethod.apply(this, args);
};
};
}
class AdminService {
userRole: string;
constructor(userRole: string) {
this.userRole = userRole;
}
@authorize("admin") // Only admins can call this
deleteDatabase() {
console.log("Deleting database...");
}
}
const admin = new AdminService("admin");
admin.deleteDatabase(); // Works
const guest = new AdminService("guest");
guest.deleteDatabase(); // Throws: "Unauthorized"
7. Advanced Concepts: Metadata Reflection
The reflect-metadata library (a polyfill for the proposed ES metadata API) lets you store and retrieve metadata about classes, properties, and parameters. This is critical for frameworks like Angular (dependency injection) and NestJS.
Setup:
Install the library:
npm install reflect-metadata
Import it at the entry point of your app:
import "reflect-metadata";
Example: Dependency Injection
Track constructor parameters with metadata to inject dependencies:
import "reflect-metadata";
const INJECTABLE_METADATA = "injectable:metadata";
// Mark a class as injectable
function Injectable() {
return function (constructor: Function) {
Reflect.defineMetadata(INJECTABLE_METADATA, true, constructor);
};
}
// Track injected dependencies
function Inject(token: string) {
return function (target: any, propertyKey: string, parameterIndex: number) {
const existingTokens = Reflect.getMetadata("inject:tokens", target) || [];
existingTokens[parameterIndex] = token;
Reflect.defineMetadata("inject:tokens", existingTokens, target);
};
}
@Injectable()
class Logger {
log(message: string) {
console.log(`[Log] ${message}`);
}
}
class UserService {
constructor(@Inject("Logger") private logger: Logger) {}
createUser(name: string) {
this.logger.log(`User ${name} created`);
return { name };
}
}
// Simple injector to resolve dependencies
function injectDependencies(constructor: Function) {
const tokens = Reflect.getMetadata("inject:tokens", constructor.prototype) || [];
const dependencies = tokens.map((token: string) => {
if (token === "Logger") return new Logger(); // Simplified resolution
throw new Error(`No provider for ${token}`);
});
return new (constructor as any)(...dependencies);
}
const userService = injectDependencies(UserService);
userService.createUser("Bob"); // Logs: "[Log] User Bob created"
8. Limitations and Considerations
- Experimental Status: Decorators are not yet standardized in JavaScript. TypeScript’s implementation may change as the proposal evolves.
- Performance: Decorators run at class definition time, which can slow down app startup if overused.
- No Direct Property Modification: Property decorators cannot modify properties directly (use metadata instead).
- Metadata Overhead:
emitDecoratorMetadataincreases bundle size by emitting type metadata.
9. Conclusion
Decorators are a versatile tool for adding reusable behavior to TypeScript code. From logging and caching to dependency injection, they enable clean, declarative patterns that reduce boilerplate. By mastering decorators, you’ll unlock advanced patterns used in modern frameworks like Angular, NestJS, and TypeORM.