Fundamentals and Applications of Decorators
Decorators are a powerful syntactic feature in TypeScript that allow extending the behavior of classes, methods, properties, or parameters through annotations without modifying the original code. They are widely used in scenarios such as logging, performance analysis, dependency injection, and can significantly improve code maintainability and reusability.
Basic Concepts of Decorators
Decorators are essentially functions that take specific parameters and return a new function or modified target. In TypeScript, decorators are categorized into the following types:
- Class Decorators
- Method Decorators
- Property Decorators
- Parameter Decorators
- Accessor Decorators
To use decorators, experimental decorator support must be enabled in tsconfig.json
:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Class Decorators
Class decorators are applied to class constructors and can be used to observe, modify, or replace class definitions. They take one parameter: the class constructor.
function logClass(target: Function) {
console.log(`Class ${target.name} is defined`);
}
@logClass
class MyClass {
constructor() {
console.log('Creating instance');
}
}
// Output: "Class MyClass is defined"
A more practical example is adding metadata to a class:
function addMetadata(metadata: object) {
return function(target: Function) {
Reflect.defineMetadata('custom:metadata', metadata, target);
};
}
@addMetadata({ version: '1.0.0', author: 'John Doe' })
class ApiService {
// ...
}
const metadata = Reflect.getMetadata('custom:metadata', ApiService);
console.log(metadata); // { version: '1.0.0', author: 'John Doe' }
Method Decorators
Method decorators are used for class methods and can intercept method calls, modify method behavior, or add additional functionality. They take three parameters:
- For static members: the class constructor; for instance members: the class prototype
- The member name
- The member's property descriptor
function logMethod(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling ${propertyKey} with args: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
console.log(`Method ${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]"
// "Method add returned: 5"
Property Decorators
Property decorators are applied to class properties and can be used to modify property behavior or add metadata. They take two parameters:
- For static members: the class constructor; for instance members: the class prototype
- The member name
function formatDate(formatString: string) {
return function(target: any, propertyKey: string) {
let value: Date;
const getter = function() {
return value.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
const setter = function(newVal: Date) {
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
};
}
class Event {
@formatDate('MMMM Do YYYY')
date: Date;
constructor(date: Date) {
this.date = date;
}
}
const event = new Event(new Date());
console.log(event.date); // "June 15, 2023" (format depends on current date)
Parameter Decorators
Parameter decorators are used for constructor or method parameters, often for dependency injection or parameter validation. They take three parameters:
- For static members: the class constructor; for instance members: the class prototype
- The member name (undefined for constructor parameters)
- The parameter's index in the function parameter list
function validateParam(min: number, max: number) {
return function(target: any, propertyKey: string, parameterIndex: number) {
const existingValidations: any[] = Reflect.getOwnMetadata('validations', target, propertyKey) || [];
existingValidations.push({
index: parameterIndex,
min,
max
});
Reflect.defineMetadata('validations', existingValidations, target, propertyKey);
};
}
function validate(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
const validations = Reflect.getOwnMetadata('validations', target, propertyKey) || [];
descriptor.value = function(...args: any[]) {
for (const validation of validations) {
const arg = args[validation.index];
if (arg < validation.min || arg > validation.max) {
throw new Error(`Parameter at index ${validation.index} must be between ${validation.min} and ${validation.max}`);
}
}
return originalMethod.apply(this, args);
};
return descriptor;
}
class MathOperations {
@validate
divide(
@validateParam(1, 100) dividend: number,
@validateParam(1, 10) divisor: number
): number {
return dividend / divisor;
}
}
const math = new MathOperations();
console.log(math.divide(50, 5)); // 10
console.log(math.divide(0, 5)); // Throws error
Decorator Factories
A decorator factory is a function that returns a decorator function, allowing decorator behavior to be configured via parameters.
function logExecutionTime(threshold: number = 0) {
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();
const duration = end - start;
if (duration > threshold) {
console.warn(`Method ${propertyKey} took ${duration.toFixed(2)}ms to execute (threshold: ${threshold}ms)`);
}
return result;
};
return descriptor;
};
}
class DataProcessor {
@logExecutionTime(10)
processLargeData() {
// Simulate time-consuming operation
for (let i = 0; i < 1000000; i++) {
Math.sqrt(i);
}
}
}
const processor = new DataProcessor();
processor.processLargeData(); // Outputs warning if execution time exceeds 10ms
Decorator Composition
Multiple decorators can be applied to the same target, and they execute in a specific order:
- Parameter decorators → Method decorators → Accessor decorators → Property decorators → Class decorators
- For the same type of decorator, execution is from bottom to top
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 ExampleClass {
@first()
@second()
method() {}
}
// Execution order:
// "second(): factory evaluated"
// "first(): factory evaluated"
// "first(): called"
// "second(): called"
Practical Use Cases
1. API Route Registration
const routes: any[] = [];
function Controller(prefix: string = '') {
return function(target: Function) {
Reflect.defineMetadata('prefix', prefix, target);
if (!Reflect.hasMetadata('routes', target)) {
Reflect.defineMetadata('routes', [], target);
}
routes.push(target);
};
}
function Get(path: string = '') {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const routes = Reflect.getMetadata('routes', target.constructor) || [];
routes.push({
method: 'get',
path,
handler: propertyKey
});
Reflect.defineMetadata('routes', routes, target.constructor);
};
}
@Controller('/users')
class UserController {
@Get('/')
getAll() {
return 'All users';
}
@Get('/:id')
getById(id: string) {
return `User ${id}`;
}
}
// Iterate over routes array to register actual routes
routes.forEach(controller => {
const prefix = Reflect.getMetadata('prefix', controller);
const controllerRoutes = Reflect.getMetadata('routes', controller);
controllerRoutes.forEach((route: any) => {
console.log(`Registering route: ${route.method.toUpperCase()} ${prefix}${route.path}`);
// In real applications, this would call the framework's route registration method
});
});
2. Form Validation
interface ValidationRule {
type: string;
message: string;
validator?: (value: any) => boolean;
}
function Validator(rules: ValidationRule[]) {
return function(target: any, propertyKey: string) {
Reflect.defineMetadata('validation', rules, target, propertyKey);
};
}
function validateForm(target: any) {
const properties = Object.getOwnPropertyNames(target);
properties.forEach(property => {
const rules = Reflect.getMetadata('validation', target, property) as ValidationRule[];
if (!rules) return;
const value = target[property];
for (const rule of rules) {
if (rule.type === 'required' && (value === undefined || value === null || value === '')) {
throw new Error(rule.message);
}
if (rule.validator && !rule.validator(value)) {
throw new Error(rule.message);
}
}
});
}
class UserForm {
@Validator([
{ type: 'required', message: 'Username is required' },
{
type: 'length',
message: 'Username must be between 3 and 20 characters',
validator: (value: string) => value.length >= 3 && value.length <= 20
}
])
username: string;
@Validator([
{ type: 'required', message: 'Email is required' },
{
type: 'pattern',
message: 'Invalid email format',
validator: (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
}
])
email: string;
constructor(data: Partial<UserForm>) {
Object.assign(this, data);
}
validate() {
validateForm(this);
}
}
const form = new UserForm({
username: 'johndoe',
email: 'john@example.com'
});
form.validate(); // Validation passes
const invalidForm = new UserForm({
username: 'jo',
email: 'invalid-email'
});
try {
invalidForm.validate(); // Throws validation error
} catch (error) {
console.error(error.message);
}
3. Performance Monitoring
function trackPerformance(metricName: string) {
return function(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args: any[]) {
const startMark = `${propertyKey}-start`;
const endMark = `${propertyKey}-end`;
performance.mark(startMark);
try {
const result = await originalMethod.apply(this, args);
return result;
} finally {
performance.mark(endMark);
performance.measure(metricName, startMark, endMark);
const measures = performance.getEntriesByName(metricName);
const lastMeasure = measures[measures.length - 1];
console.log(`[Performance] ${metricName}: ${lastMeasure.duration.toFixed(2)}ms`);
// Performance data can be sent to a monitoring system here
}
};
return descriptor;
};
}
class AnalyticsService {
@trackPerformance('fetchAnalyticsData')
async fetchData() {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, Math.random() * 1000));
return { data: 'sample' };
}
}
const analytics = new AnalyticsService();
analytics.fetchData(); // Outputs performance data
Decorators and Metadata Reflection
TypeScript decorators can be combined with the Reflection Metadata API for more powerful functionality. Install the reflect-metadata
package:
npm install reflect-metadata
Then import it at the application entry point:
import 'reflect-metadata';
Example: Dependency Injection Container
type Constructor<T = any> = new (...args: any[]) => T;
const Injectable = (): ClassDecorator => target => {};
class Container {
private instances = new Map<Constructor, any>();
register<T>(token: Constructor<T>, instance: T) {
this.instances.set(token, instance);
}
resolve<T>(token: Constructor<T>): T {
if (this.instances.has(token)) {
return this.instances.get(token);
}
const params = Reflect.getMetadata('design:paramtypes', token) || [];
const injections = params.map(param => this.resolve(param));
const instance = new token(...injections);
this.instances.set(token, instance);
return instance;
}
}
@Injectable()
class DatabaseService {
connect() {
console.log('Connected to database');
}
}
@Injectable()
class UserRepository {
constructor(private database: DatabaseService) {}
getUsers() {
this.database.connect();
return ['user1', 'user2'];
}
}
const container = new Container();
const userRepo = container.resolve(UserRepository);
console.log(userRepo.getUsers()); // ["user1", "user2"]
Limitations of Decorators
While decorators are powerful, they have some limitations and considerations:
- Execution Order: When multiple decorators are applied to the same target, the execution order may not be intuitive
- Debugging Difficulty: Decorators modify original behavior, which can make debugging challenging
- Performance Impact: Complex decorator logic may affect performance
- Type Information: Decorators can complicate the type system, requiring additional type handling
- Proposal Stage: Decorators are still an ECMAScript proposal and may change in the future
Advanced Techniques
Dynamic Decorator Application
function conditionalDecorator(condition: boolean, decorator: MethodDecorator) {
return function(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
if (condition) {
return decorator(target, propertyKey, descriptor);
}
return descriptor;
};
}
class FeatureToggle {
@conditionalDecorator(
process.env.NODE_ENV === 'development',
logMethod
)
experimentalMethod() {
// Only logs in development environment
}
}
Decorator Composition Utility
function composeDecorators(...decorators: MethodDecorator[]) {
return function(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
return decorators.reduce((currentDescriptor, decorator) => {
return decorator(target, propertyKey, currentDescriptor) || currentDescriptor;
}, descriptor);
};
}
class MultiDecorated {
@composeDecorators(
logMethod,
trackPerformance('importantOperation')
)
importantOperation() {
// Applies both logging and performance tracking
}
}
Decorators with Generics
function typeCheck<T>() {
return function(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function(arg: T) {
if (typeof arg !== typeof {} as T) {
throw new Error(`Invalid type: expected ${typeof {} as T}, got ${typeof arg}`);
}
return originalMethod.call(this, arg);
};
return descriptor;
};
}
class GenericExample {
@typeCheck<number>()
processNumber(num: number) {
return num * 2;
}
}
const example = new GenericExample();
console.log(example.processNumber(5)); // 10
console.log(example.processNumber('5' as any)); // Throws type error
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:参数属性
下一篇:混入(Mixins)模式实现