TypeScript Decorators Cheat Sheet
Covers enabling legacy experimental decorators, writing class, method, property, and parameter decorators, and their execution order.
Enabling Decorators (tsconfig.json)
Legacy decorators must be turned on explicitly in the compiler options.
{ "compilerOptions": { "target": "ES2020", "experimentalDecorators": true, "emitDecoratorMetadata": true }}
Class Decorators
Wrap or extend a class definition using a decorator factory.
function Logger(prefix: string) { // decorator factory - returns the actual decorator return function <T extends { new (...args: any[]): {} }>(constructor: T) { return class extends constructor { createdAt = new Date(); constructor(...args: any[]) { super(...args); console.log(`${prefix}: instance created`); } }; };}@Logger('UserService')class UserService { constructor(public name: string) {}}
Method & Property Decorators
Intercept method calls or mark properties using the descriptor API.
function LogCall(target: any, key: string, descriptor: PropertyDescriptor) { const original = descriptor.value; descriptor.value = function (...args: any[]) { console.log(`Calling ${key} with`, args); return original.apply(this, args); // preserve original behavior }; return descriptor;}class Calculator { @LogCall add(a: number, b: number) { return a + b; }}// Property decorator - receives target and property key onlyfunction Required(target: any, propertyKey: string) { // typically used with reflect-metadata to store validation rules}
Parameter Decorators
Tag individual method parameters, commonly used for DI frameworks.
function LogParam(target: any, methodName: string, paramIndex: number) { console.log(`Param ${paramIndex} of ${methodName} decorated`);}class Greeter { greet(@LogParam name: string) { return `Hello, ${name}`; }}
Decorator Kinds & Execution Order
The five decorator targets and how they're applied.
- Class decorator- Applied to the constructor; can replace/extend the class definition
- Method decorator- Receives (target, propertyKey, descriptor); can wrap or replace the method
- Accessor decorator- Applies to a get/set pair, same signature as method decorators
- Property decorator- Receives (target, propertyKey) only, no descriptor
- Parameter decorator- Receives (target, propertyKey, parameterIndex)
- Evaluation order- Parameter/method/accessor/property decorators run before class decorators, bottom-up within a declaration
- Decorator factory- A function that returns a decorator, allowing arguments like @Logger('X')
TC39 Stage 3 Decorators (TS 5.0+ default)
The standardized decorator proposal shipped without experimentalDecorators has a different signature based on a context object.
function loggedMethod(originalMethod: any, context: ClassMethodDecoratorContext) { const methodName = String(context.name); function replacementMethod(this: any, ...args: any[]) { console.log(`Entering ${methodName}`); const result = originalMethod.call(this, ...args); console.log(`Exiting ${methodName}`); return result; } return replacementMethod;}class Person { name: string; constructor(name: string) { this.name = name; } @loggedMethod greet() { return `Hello, ${this.name}`; }}// context carries { kind, name, static, private, addInitializer, ... }// - no target/descriptor mutation, decorators return a replacement value instead
Reflect Metadata for Dependency Injection
Legacy decorators combine with reflect-metadata to build a minimal DI container.
import 'reflect-metadata';const INJECTABLE = Symbol('injectable');function Injectable(): ClassDecorator { return (target) => { Reflect.defineMetadata(INJECTABLE, true, target); };}@Injectable()class Logger { log(msg: string) { console.log(msg); } }@Injectable()class UserService { constructor(private logger: Logger) {}}function resolve<T>(target: new (...args: any[]) => T): T { // emitDecoratorMetadata writes 'design:paramtypes' automatically const paramTypes: any[] = Reflect.getMetadata('design:paramtypes', target) || []; const deps = paramTypes.map((dep) => resolve(dep)); return new target(...deps);}const service = resolve(UserService); // Logger auto-injected
Accessor Decorators for Validation
Wrap a get/set pair to enforce invariants transparently at assignment time.
function Positive(target: any, key: string, descriptor: PropertyDescriptor) { const original = descriptor.set!; descriptor.set = function (value: number) { if (value < 0) throw new RangeError(`${key} must be non-negative`); original.call(this, value); }; return descriptor;}class Account { private _balance = 0; get balance() { return this._balance; } @Positive set balance(value: number) { this._balance = value; }}const acct = new Account();acct.balance = 100; // OK// acct.balance = -5; // throws RangeError
Composing Multiple Decorators
Stacked decorators apply bottom-up but their factory calls evaluate top-down.
function trace(label: string) { console.log(`factory evaluated: ${label}`); // runs top-down, at decoration time return function (target: any, key: string, descriptor: PropertyDescriptor) { const original = descriptor.value; descriptor.value = function (...args: any[]) { console.log(`enter ${label}`); // runs bottom-up, at call time const result = original.apply(this, args); console.log(`exit ${label}`); return result; }; };}class Pipeline { @trace('outer') @trace('inner') run() { return 'done'; }}// Evaluation order: 'outer' factory, 'inner' factory// Call order on run(): enter outer -> enter inner -> exit inner -> exit outer
Real-World Decorator Idioms
Patterns decorators enable in frameworks like Angular, NestJS, and TypeORM.
- Route decorators- @Get('/users'), @Post('/users') on controller methods register HTTP handlers declaratively (NestJS pattern)
- Dependency injection tokens- @Injectable() marks a class as constructible by a DI container; @Inject(TOKEN) overrides the default resolution
- ORM column mapping- @Entity(), @Column(), @PrimaryGeneratedColumn() attach persistence metadata without polluting business logic
- Memoization decorator- A method decorator that caches return values keyed by JSON.stringify(args), replacing the descriptor.value function
- addInitializer (Stage 3)- context.addInitializer() registers a callback that runs once per instance, used to bind methods or auto-register instances
- Deprecation warnings- A method decorator that logs console.warn on first call, useful for flagging legacy APIs during migration
- Class field decorators (Stage 3)- ClassFieldDecoratorContext lets a decorator return an initializer function that transforms the field's initial value
Legacy TS decorators (experimentalDecorators) and the new TC39 Stage 3 decorators (default in TS 5.0+ without the flag) have different signatures and are not interchangeable - check your tsconfig before copying decorator code from another project.