TypeScript decorators provide a powerful way to modify or enhance classes and their members. Let's explore advanced decorator patterns and their practical applications.
Understanding Decorators
Decorators are special declarations that can modify classes and their members:
- Class Decorators
- Method Decorators
- Property Decorators
- Parameter Decorators
- Accessor Decorators
Basic Implementation
1. Class Decorators
Create simple class decorators:
function logger(target: Function) {
// Add logging functionality
target.prototype.log = function(message: string) {
console.log(`[${target.name}]: ${message}`);
};
}
@logger
class Example {
method() {
(this as any).log('Method called');
}
}
2. Method Decorators
Implement method decoration:
function measure() {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();
console.log(`${propertyKey} took ${end - start}ms`);
return result;
};
return descriptor;
};
}
class API {
@measure()
async fetchData() {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
return { data: 'result' };
}
}
Advanced Patterns
1. Property Validation
Implement property validation decorators:
function validate(validationFn: (value: any) => boolean) {
return function (target: any, propertyKey: string) {
let value: any;
const getter = function () {
return value;
};
const setter = function (newValue: any) {
if (!validationFn(newValue)) {
throw new Error(`Invalid value for ${propertyKey}`);
}
value = newValue;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
};
}
class User {
@validate(email => /^[^@]+@[^@]+\.[^@]+$/.test(email))
email: string;
@validate(age => age >= 0 && age <= 120)
age: number;
}
2. Dependency Injection
Create a simple DI container:
const Injectable = (): ClassDecorator => {
return target => {
Reflect.defineMetadata('injectable', true, target);
};
};
class Container {
private static instances = new Map<any, any>();
static resolve<T>(target: new (...args: any[]) => T): T {
if (Container.instances.has(target)) {
return Container.instances.get(target);
}
const tokens = Reflect.getMetadata('design:paramtypes', target) || [];
const injections = tokens.map((token: any) => Container.resolve(token));
const instance = new target(...injections);
Container.instances.set(target, instance);
return instance;
}
}
Performance Optimization
1. Memoization Decorator
Implement function result caching:
function memoize() {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
const cache = new Map<string, any>();
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;
};
return descriptor;
};
}
2. Debounce Decorator
Implement method debouncing:
function debounce(delay: number) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
let timeoutId: NodeJS.Timeout;
descriptor.value = function (...args: any[]) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
originalMethod.apply(this, args);
}, delay);
};
return descriptor;
};
}
Error Handling
1. Error Boundary Decorator
Implement error handling:
function errorBoundary(errorHandler: (error: Error) => void) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
errorHandler(error);
throw error;
}
};
return descriptor;
};
}
2. Retry Decorator
Implement automatic retry logic:
function retry(attempts: number, delay: number) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
for (let i = 0; i < attempts; i++) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
if (i === attempts - 1) throw error;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
};
return descriptor;
};
}
Metadata Reflection
1. Custom Metadata
Work with custom metadata:
function addMetadata(metadata: any): ClassDecorator {
return target => {
Reflect.defineMetadata('custom:metadata', metadata, target);
};
}
function getMetadata(target: any) {
return Reflect.getMetadata('custom:metadata', target);
}
@addMetadata({ version: '1.0.0' })
class Example {
// Class implementation
}
2. Parameter Decorators
Implement parameter validation:
function required() {
return function (
target: any,
propertyKey: string,
parameterIndex: number
) {
const existingRequired: number[] =
Reflect.getMetadata('required', target, propertyKey) || [];
existingRequired.push(parameterIndex);
Reflect.defineMetadata('required', existingRequired, target, propertyKey);
};
}
class API {
fetchUser(@required() id: string) {
return `Fetching user ${id}`;
}
}
Best Practices
1. Composition
Combine multiple decorators:
function compose(...decorators: MethodDecorator[]) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
return decorators.reduceRight(
(desc, decorator) => decorator(target, propertyKey, desc),
descriptor
);
};
}
class Example {
@compose(
measure(),
errorBoundary(console.error),
retry(3, 1000)
)
async method() {
// Method implementation
}
}
2. Type Safety
Ensure type safety in decorators:
function typed<T>() {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const result = originalMethod.apply(this, args);
if (!(result instanceof Promise)) {
throw new Error(`${propertyKey} must return a Promise<T>`);
}
return result as Promise<T>;
};
return descriptor;
};
}
Conclusion
TypeScript decorators provide powerful metaprogramming capabilities:
- Benefits
- Code reusability
- Aspect-oriented programming
- Clean architecture
- Enhanced maintainability
- Implementation Tips
- Use composition
- Ensure type safety
- Handle errors properly
- Consider performance
- Best Practices
- Keep decorators focused
- Document behavior
- Test thoroughly
- Consider side effects
Remember to:
- Use decorators judiciously
- Maintain type safety
- Handle errors properly
- Test decorator behavior