Technology · TypeScript
TypeScript Decorators
Understand decorators, metadata, class transformations, and real-world patterns.
TL;DR
- 01Use decorators to annotate and modify classes and methods.
- 02Enable experimentalDecorators in tsconfig.json to use them.
- 03Apply metadata to decorate classes for frameworks and libraries.
Class Decorators
- Enable decorators in tsconfig.json first.
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true } } - Create a class decorator that receives the class constructor.
function Sealed(constructor: Function) { Object.seal(constructor); Object.seal(constructor.prototype); } @Sealed class User { name = "Alice"; } - Modify or wrap classes to add functionality.
function Timestamped<T extends { new(...args: any[]): {} }>( constructor: T ) { return class extends constructor { createdAt = new Date(); }; } - Factory decorators can return new classes with enhanced features.
Method and Property Decorators
- Create method decorators to intercept function calls.
function LogCalls( target: any, propertyKey: string, descriptor: PropertyDescriptor ) { const original = descriptor.value; descriptor.value = function(...args: any[]) { console.log(`Calling ${propertyKey}`, args); return original.apply(this, args); }; } class User { @LogCalls setName(name: string) { this.name = name; } } - Property decorators receive the target and property name.
function Validate(target: any, propertyKey: string) { let value: any; Object.defineProperty(target, propertyKey, { get() { return value; }, set(newVal: any) { if (newVal.length < 3) throw new Error("Too short"); value = newVal; } }); }
Metadata and Reflection
- Store metadata on classes and methods using the reflect-metadata library.
import "reflect-metadata"; function Route(path: string) { return function(target: any, propertyKey: string) { Reflect.defineMetadata("route:path", path, target, propertyKey); }; } class Controller { @Route("/users") getUsers() { } } - Retrieve metadata to build frameworks and libraries.
const path = Reflect.getMetadata("route:path", Controller.prototype, "getUsers"); console.log(path); // "/users" - Use emitDecoratorMetadata to capture parameter and return types.
function ValidateTypes(target: any, propertyKey: string) { const types = Reflect.getMetadata("design:paramtypes", target, propertyKey); // Types are [String, Number, etc.] }
Decorator Composition
- Stack multiple decorators on the same class or method.
@Sealed @Timestamped class User { @LogCalls @Validate setName(name: string) { this.name = name; } } - Decorators execute from bottom to top on the target.
- Compose decorators to build complex functionality.
function Chain(...decorators: any[]) { return (target: any, propertyKey: string) => { decorators.forEach(d => d(target, propertyKey)); }; } - Use composition to keep decorators focused on single concerns.
Real-World Patterns
- Build validation decorators for class properties.
function MinLength(min: number) { return function(target: any, propertyKey: string) { Reflect.defineMetadata(`validate:${propertyKey}`, { min }, target); }; } class User { @MinLength(3) name: string; } - Create caching decorators for expensive methods.
function Memoize(target: any, propertyKey: string, descriptor: PropertyDescriptor) { const original = descriptor.value; const cache = new Map(); descriptor.value = function(...args: any[]) { const key = JSON.stringify(args); if (cache.has(key)) return cache.get(key); const result = original.apply(this, args); cache.set(key, result); return result; }; } - Use decorators in frameworks like NestJS for dependency injection.
Tip: Use decorators sparingly and for cross-cutting concerns like logging, validation, or caching — avoid using them for simple business logic.
Warning: Decorators are experimental and may change in future TypeScript versions, so use with caution in production code.