TypeScript Decorator Patterns
Implement class, method, and property decorators with reflect-metadata in TypeScript
When to Use
- Adding cross-cutting concerns (logging, validation, caching) to classes and methods
- Working with frameworks that use decorators (NestJS, TypeORM, Angular)
- Implementing dependency injection with metadata reflection
- Creating reusable, declarative annotations for class members
Instructions
- Enable decorators in
tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
- Class decorator — wraps or replaces the class constructor:
function Sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@Sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
}
- Method decorator — intercepts method calls:
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with`, args);
const result = original.apply(this, args);
console.log(`${propertyKey} returned`, result);
return result;
};
}
class Calculator {
@Log
add(a: number, b: number): number {
return a + b;
}
}
- Property decorator:
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} chars`);
value = newValue;
},
});
};
}
class User {
@MinLength(3)
name: string = '';
}
- Parameter decorator — marks parameters for injection or validation:
function Inject(token: string) {
return function (target: any, propertyKey: string | undefined, parameterIndex: number) {
const existingInjections = Reflect.getOwnMetadata('injections', target) || [];
existingInjections.push({ index: parameterIndex, token });
Reflect.defineMetadata('injections', existingInjections, target);
};
}
- Decorator factory — return a decorator from a function to accept configuration:
function Retry(attempts: number, delay: number) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = async function (...args: any[]) {
for (let i = 0; i < attempts; i++) {
try {
return await original.apply(this, args);
} catch (e) {
if (i === attempts - 1) throw e;
await new Promise((r) => setTimeout(r, delay));
}
}
};
};
}
class ApiClient {
@Retry(3, 1000)
async fetchData(): Promise<Data> {
/* ... */
}
}
- Decorator composition — decorators apply bottom-up:
class Service {
@Log // Applied second (outer)
@Retry(3, 100) // Applied first (inner)
async getData(): Promise<Data> {
/* ... */
}
}
Details
TypeScript decorators are an implementation of the TC39 Stage 3 decorators proposal (legacy version). They execute at class definition time, not at runtime method invocation.
Decorator execution order:
- Parameter decorators (left to right per parameter)
- Method and property decorators (top to bottom per class member)
- Class decorators (bottom to top if multiple)
emitDecoratorMetadata uses reflect-metadata to emit type information that decorators can read at runtime. This powers dependency injection in NestJS and TypeORM. Install reflect-metadata and import it once at the entry point.
TC39 Stage 3 decorators (2023+): TypeScript 5.0+ supports the new standard decorators syntax WITHOUT experimentalDecorators. The new syntax is different from the legacy syntax — they use a different function signature and do not support parameter decorators or metadata emission by default.
Legacy vs Stage 3:
- Legacy:
function Dec(target, key, descriptor)— widely used in NestJS, Angular, TypeORM - Stage 3:
function Dec(value, context)— new standard, limited framework adoption so far - If you use NestJS or Angular, continue with
experimentalDecorators: true
Trade-offs:
- Decorators add powerful metaprogramming capabilities — but hide behavior behind annotations
emitDecoratorMetadataincreases bundle size with reflection metadata- Decorator type safety is limited — the decorator function signature uses
anyfor targets - Debuggability suffers when multiple decorators transform a method — stack traces point to the decorator wrapper, not the original method
Source
https://typescriptlang.org/docs/handbook/decorators.html
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.