TypeScript Decorators: Metaprogramming Patterns for Clean Code
Decorators let you attach behavior to classes, methods, and properties without modifying their internals. They're used everywhere in NestJS, TypeORM, and Angular — and for good reason.
What Decorators Are
A decorator is a function that wraps another function, class, or property. The @ syntax is sugar for passing the target as an argument:
@Log // is equivalent to: MyClass = Log(MyClass)
class MyClass {}
Method Decorators
function Retry(attempts: number) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
let lastError: Error;
for (let i = 0; i < attempts; i++) {
try {
return await originalMethod.apply(this, args);
} catch (err) {
lastError = err as Error;
if (i < attempts - 1) {
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
}
}
}
throw lastError!;
};
return descriptor;
};
}
class PaymentService {
@Retry(3) // Automatically retries with exponential backoff
async chargeCard(amount: number) {
return await stripeClient.charge(amount);
}
}
Class Decorators
function Singleton<T extends { new(...args: any[]): {} }>(constructor: T) {
let instance: InstanceType<T>;
return class extends constructor {
constructor(...args: any[]) {
super(...args);
if (instance) return instance;
instance = this as InstanceType<T>;
}
} as T;
}
@Singleton
class DatabaseConnection {
private pool = createPool();
// Guaranteed single instance across the app
}
Property Decorators for Validation
function MinLength(min: number) {
return function (target: any, propertyKey: string) {
let value: string;
Object.defineProperty(target, propertyKey, {
get: () => value,
set: (newValue: string) => {
if (newValue.length < min) {
throw new Error(`${propertyKey} must be at least ${min} characters`);
}
value = newValue;
},
});
};
}
class User {
@MinLength(8)
password: string = '';
}
Real-World: Logging Decorator
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = async function (...args: any[]) {
const start = Date.now();
console.log(`[${propertyKey}] called with`, args);
try {
const result = await original.apply(this, args);
console.log(`[${propertyKey}] completed in ${Date.now() - start}ms`);
return result;
} catch (err) {
console.error(`[${propertyKey}] failed after ${Date.now() - start}ms:`, err);
throw err;
}
};
}
class OrderService {
@Log
async createOrder(data: CreateOrderDto) { /* ... */ }
}
Enable Decorators
// tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Decorators — alongside dependency injection, module patterns, and clean architecture — are part of the advanced TypeScript patterns in the Ship Fast Skill Pack skill set.
Top comments (0)