Decorator Fundamentals
Decorator Fundamentals
Class Decorator
function Sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@Sealed
class MyClass {
property = 'value';
method() { return 42; }
}
// Cannot add new properties or methods
// (MyClass.prototype as any).newMethod = () => {}; // Error in runtime
Method Decorator
function Log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with args:`, args);
const result = originalMethod.apply(this, args);
console.log(`${propertyKey} returned:`, result);
return result;
};
}
class Calculator {
@Log
add(a: number, b: number): number {
return a + b;
}
}
new Calculator().add(2, 3);
// Calling add with args: [2, 3]
// add returned: 5
Decorator Factories
Decorator Factories
Configurable Decorators
function Throttle(delay: number) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
let lastCall = 0;
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
return originalMethod.apply(this, args);
}
};
};
}
class SearchService {
@Throttle(300)
search(query: string): void {
console.log(`Searching: ${query}`);
}
}
Property Decorator
function Required(target: any, propertyKey: string) {
let value: any;
const getter = () => value;
const setter = (newVal: any) => {
if (newVal === undefined || newVal === null) {
throw new Error(`${propertyKey} is required`);
}
value = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
class User {
@Required
name!: string;
@Required
email!: string;
}
const user = new User();
user.name = 'Alice'; // OK
// user.name = null; // Error: name is required
Practical Decorators
Practical Decorators
Memoize Decorator
function Memoize(
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;
};
}
class MathService {
@Memoize
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
Validate Decorator
function Validate(schema: ZodSchema) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const result = schema.safeParse(args[0]);
if (!result.success) {
throw new Error(`Validation failed: ${result.error.message}`);
}
return originalMethod.apply(this, args);
};
};
}
Retry Decorator
function Retry(maxRetries: number = 3, delay: number = 1000) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
for (let i = 0; i <= maxRetries; i++) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
if (i === maxRetries) throw error;
await new Promise(r => setTimeout(r, delay * (i + 1)));
}
}
};
};
}
TC39 Decorators
TC39 Decorators
New Standard vs Legacy
// Legacy decorators (experimentalDecorators: true in tsconfig)
function Log(target: any, key: string, descriptor: PropertyDescriptor) { /* ... */ }
// TC39 decorators (new standard)
function Log<T extends (...args: any[]) => any>(
originalMethod: T,
context: ClassMethodDecoratorContext
) {
return function (this: any, ...args: Parameters<T>): ReturnType<T> {
console.log(`Calling ${String(context.name)}`);
return originalMethod.call(this, ...args);
};
}
class UserService {
@Log
getUser(id: string) {
return { id, name: 'Alice' };
}
}
TC39 Access Decorators
function Override(original: any, context: ClassAccessorDecoratorContext) {
return {
get(this: any) {
return original.get.call(this);
},
set(this: any, value: any) {
// Custom logic
original.set.call(this, value);
}
};
}
class Settings {
@Override
accessor theme: string = 'light';
}
Configuration
// tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true, // legacy mode
"emitDecoratorMetadata": true // reflect metadata
}
}
Best Practices
- Keep decorators focused on single responsibility
- Document decorator behavior clearly
- Use decorator factories for configuration
- Prefer TC39 decorators for new projects
- Test decorator behavior independently