Advanced decorator patterns and metadata.
Parameter Decorators with Metadata
import "reflect-metadata";
const requiredMetadataKey = Symbol("required");
function required(
target: any,
propertyKey: string | symbol,
parameterIndex: number
) {
const existingRequiredParameters: number[] =
Reflect.getMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(
requiredMetadataKey,
existingRequiredParameters,
target,
propertyKey
);
}
function validate(
target: any,
propertyName: string,
descriptor: PropertyDescriptor
) {
const method = descriptor.value;
const requiredParameters: number[] = Reflect.getMetadata(
requiredMetadataKey,
target,
propertyName
);
descriptor.value = function (...args: any[]) {
if (requiredParameters) {
for (const parameterIndex of requiredParameters) {
if (parameterIndex >= args.length || args[parameterIndex] == null) {
throw new Error(`Parameter at index ${parameterIndex} is required`);
}
}
}
return method.apply(this, args);
};
}
class UserService {
@validate
createUser(@required name: string, age: number) {
// ...
}
}
Class Decorator Factory
function Entity(tableName: string) {
return function <T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
static tableName = tableName;
id: number = 0;
};
};
}
@Entity("users")
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
console.log((User as any).tableName); // "users"
Method Decorator with Caching
const cache = new Map<string, any>();
function cacheResult(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const key = `${propertyKey}_${JSON.stringify(args)}`;
if (cache.has(key)) {
return cache.get(key);
}
const result = originalMethod.apply(this, args);
cache.set(key, result);
return result;
};
return descriptor;
}
class Calculator {
@cacheResult
expensiveOperation(n: number): number {
// Expensive computation
return n * n;
}
}
Property Decorator with Validation
function validateRange(min: number, max: number) {
return function (target: any, propertyKey: string) {
let value: number;
const getter = () => value;
const setter = (newVal: number) => {
if (newVal < min || newVal > max) {
throw new Error(
`Value must be between ${min} and ${max}, got ${newVal}`
);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
};
}
class Product {
@validateRange(0, 100)
discount: number = 0;
}
Multiple Decorators
function first() {
console.log("first(): factory evaluated");
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log("first(): called");
};
}
function second() {
console.log("second(): factory evaluated");
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log("second(): called");
};
}
class Example {
@first()
@second()
method() {}
}
// Output:
// first(): factory evaluated
// second(): factory evaluated
// second(): called
// first(): called
Exercises
Exercise 1: Timing Decorator
// Create a decorator that:
// - Measures execution time
// - Logs the duration
// - Works with async methods
Exercise 2: Retry Decorator
// Create a decorator that:
// - Retries a method on failure
// - Takes max retries as parameter
// - Handles errors appropriately
Exercise 3: Logging Decorator
// Create a decorator that:
// - Logs method calls with parameters
// - Logs return values
// - Can be enabled/disabled
Exercise 4: Authorization Decorator
// Create a decorator that:
// - Checks user permissions
// - Throws error if unauthorized
// - Works with role-based access
Exercise 5: Transaction Decorator
// Create a decorator that:
// - Wraps method in a transaction
// - Rolls back on error
// - Commits on success
Best Practices
- Use decorator factories - For configurable decorators
- Store metadata properly - Use reflect-metadata
- Handle errors - Don't let decorators break silently
- Document behavior - Explain what decorators do
- Test thoroughly - Ensure decorators work correctly
Common Mistakes
- Forgetting to return descriptor
// ❌ Wrong
function decorator(target: any, key: string, desc: PropertyDescriptor) {
desc.value = function() { /* ... */ };
// Missing return
}
// ✅ Correct
function decorator(target: any, key: string, desc: PropertyDescriptor) {
desc.value = function() { /* ... */ };
return desc;
}
- Not handling async methods
// ❌ Doesn't handle promises
function decorator(target: any, key: string, desc: PropertyDescriptor) {
const result = desc.value();
// result might be a Promise
}
// ✅ Handle async
function decorator(target: any, key: string, desc: PropertyDescriptor) {
const original = desc.value;
desc.value = async function(...args: any[]) {
return await original.apply(this, args);
};
}
- Circular dependencies
// ❌ Might cause issues
@decoratorA
class A {
@decoratorB
method() {}
}
Next Steps
Learn about Module Augmentation.