TypeScript Decorators
Understand decorators, metadata, class transformations, and real-world patterns.
TL;DR
- 01Use decorators to annotate and modify classes and methods.
- 02TypeScript 5.0+ supports standard decorators natively — use
experimentalDecoratorsonly for legacy frameworks (NestJS/Angular with older setups) - 03Apply metadata to decorate classes for frameworks and libraries.
Tips
- 01Use decorators sparingly and for cross-cutting concerns like logging, validation, or caching — avoid using them for simple business logic.
Warnings
- 01Standard decorators (TypeScript 5.0+, TC39 Stage 3) are stable. Only use
experimentalDecorators: trueif you're working with NestJS, Angular, or other frameworks that haven't yet migrated to the standard.
Class Decorators
- Standard decorators work in TypeScript 5.0+ without any tsconfig flag. Use
experimentalDecoratorsonly for legacy frameworks (NestJS, Angular with older setups).# TypeScript 5.0+: no flag needed for standard decorators # Legacy only (NestJS / Angular): - Enable legacy decorators in tsconfig.json when required by your framework.
{ "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.
FAQ
Add "experimentalDecorators": true to your tsconfig.json compilerOptions. If you need runtime metadata (e.g., for dependency injection), also enable "emitDecoratorMetadata": true and install the reflect-metadata package.
A class decorator receives the constructor function and can replace or modify the entire class, while a method decorator receives the target prototype, the method name, and its property descriptor — making it better suited for wrapping or intercepting individual method calls.
When composing decorators, they are applied bottom-up (innermost first) but evaluated top-down, so @A @B on a class means B's factory runs first but A's decorator function wraps the result last — the outermost decorator has the final say on the output.
Yes — combine a parameter decorator with a method decorator: the parameter decorator marks which params need validation using Reflect.metadata, and the method decorator wraps the function to check those marked params before execution runs.
No — TypeScript's legacy decorators (experimentalDecorators) follow an older spec and behave differently from the Stage 3 TC39 proposal. TypeScript 5.0+ supports the new standard decorators without the flag, but the APIs are incompatible, so migrating existing decorator code requires careful refactoring.