Method decorators are a powerful TypeScript feature that allows you to modify or enhance class methods at runtime. This guide will show you how to leverage method decorators to write more maintainable and feature-rich TypeScript applications.
Table of Contents
- What are Method Decorators?
- Basic Method Decorator Syntax
- Understanding Decorator Parameters
- Creating Your First Method Decorator
- Method Decorator Factory Pattern
- Practical Use Cases
- Best Practices
- Common Pitfalls and Solutions
- Integration with Other TypeScript Features
- Testing Decorated Methods
What are Method Decorators?
Method decorators are special functions that can modify, observe, or replace class methods. They run when the method is defined, not when it’s called, allowing you to change or augment the method’s behavior during the class definition phase.
Basic Method Decorator Syntax
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Store the original method
const originalMethod = descriptor.value;
// Replace the method with new functionality
descriptor.value = function (...args: any[]) {
console.log(`Calling method ${propertyKey} with args: ${args}`);
return originalMethod.apply(this, args);
};
return descriptor;
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}
Code language: JavaScript (javascript)
Understanding Decorator Parameters
Method decorators receive three parameters:
target
: The prototype of the class (for instance methods) or the constructor function (for static methods)propertyKey
: The name of the method being decorateddescriptor
: A PropertyDescriptor object that describes the method
Creating Your First Method Decorator
Let’s create a simple timing decorator that measures method execution time:
function timing() {
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 to execute`);
return result;
};
return descriptor;
};
}
class DataProcessor {
@timing()
processData(data: number[]): number {
return data.reduce((acc, curr) => acc + curr, 0);
}
}
Code language: JavaScript (javascript)
Method Decorator Factory Pattern
Decorator factories allow you to customize decorator behavior by passing parameters:
function validate(validationFn: (value: any) => boolean) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
if (!args.every(validationFn)) {
throw new Error(`Invalid arguments for ${propertyKey}`);
}
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class NumberCalculator {
@validate((n) => typeof n === 'number' && !isNaN(n))
multiply(a: number, b: number): number {
return a * b;
}
}
Code language: JavaScript (javascript)
Practical Use Cases
1. Authentication Decorator
function requireAuth(roles: string[] = []) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const user = getCurrentUser(); // Implementation depends on your auth system
if (!user) {
throw new Error('User not authenticated');
}
if (roles.length && !roles.includes(user.role)) {
throw new Error('Insufficient permissions');
}
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class UserService {
@requireAuth(['admin'])
deleteUser(userId: string): void {
// Delete user logic
}
}
Code language: JavaScript (javascript)
2. Caching Decorator
function memoize() {
const cache = new Map();
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
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;
};
return descriptor;
};
}
class ExpensiveOperations {
@memoize()
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
Code language: JavaScript (javascript)
Best Practices
Keep Decorators Single-Purpose: Each decorator should focus on one specific functionality.
Handle Errors Gracefully: Always include proper error handling in your decorators.
function errorBoundary() {
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) {
console.error(`Error in ${propertyKey}:`, error);
throw error;
}
};
return descriptor;
};
}
Code language: JavaScript (javascript)
- Maintain Type Safety: Use TypeScript’s type system to ensure decorator type safety.
Common Pitfalls and Solutions
Decorator Order
When using multiple decorators, remember they are applied from bottom to top:
class Example {
@decoratorA
@decoratorB
method() {}
// decoratorB is applied first, then decoratorA
}
Code language: JavaScript (javascript)
this
Context
Be careful with the this
context in decorators. Always use apply
or call
when invoking the original method:
descriptor.value = function (...args: any[]) {
// Correct: Preserves 'this' context
return originalMethod.apply(this, args);
// Incorrect: Loses 'this' context
return originalMethod(...args);
};
Code language: JavaScript (javascript)
Integration with Other TypeScript Features
Method decorators work well with other TypeScript features:
class ApiService {
@log
@errorBoundary()
async fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json();
}
}
Code language: JavaScript (javascript)
Testing Decorated Methods
When testing decorated methods, consider testing both the decorated and undecorated versions:
describe('Calculator', () => {
it('should add numbers correctly with logging', () => {
const calc = new Calculator();
const result = calc.add(2, 3);
expect(result).toBe(5);
});
it('should preserve original method behavior', () => {
// Access original method if needed for testing
const originalAdd = Calculator.prototype.add;
expect(originalAdd.call(null, 2, 3)).toBe(5);
});
});
Code language: PHP (php)
Method decorators are a powerful tool in TypeScript that can help you write more maintainable and feature-rich applications. By following these patterns and best practices, you can effectively use decorators to enhance your class methods while maintaining clean and maintainable code.
Remember to check TypeScript’s official documentation for updates on decorator syntax, as the feature is still evolving with newer versions of the language.