Decorators
A decorator is a function you attach to a class or class member with @name syntax. It can observe, wrap or replace what it's attached to — adding logging, validation, caching or registration without cluttering the code itself. TypeScript 5.0 supports the standard ECMAScript decorators proposal.
A first method decorator
function logged(original: Function, context: ClassMethodDecoratorContext) { const name = String(context.name); return function (this: unknown, ...args: unknown[]) { console.log(`→ ${name}(${args.join(", ")})`); const result = original.apply(this, args); console.log(`← ${name} returned ${result}`); return result; }; } class Calculator { @logged add(a: number, b: number) { return a + b; } } new Calculator().add(2, 3);
→ add(2, 3) ← add returned 5
A method decorator receives two arguments: the original method and a context object describing it (its name, whether it's static or private, and more). If it returns a function, that function replaces the method. Here, the replacement logs around a call to the original.
Typing a decorator properly
The version above uses loose types. A generic version preserves the method's real signature:
function timed<This, Args extends unknown[], Return>( target: (this: This, ...args: Args) => Return, context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>, ) { return function (this: This, ...args: Args): Return { const start = performance.now(); const result = target.call(this, ...args); const ms = performance.now() - start; console.log(`${String(context.name)} took ${ms < 50 ? "<50" : Math.round(ms)}ms`); return result; }; } class Report { @timed build(rows: number) { let total = 0; for (let i = 0; i < rows; i++) total += i; return total; } } console.log(new Report().build(1000));
build took <50ms 499500
Now the compiler would reject applying @timed somewhere it doesn't fit, and the decorated method keeps its parameter and return types.
Decorator factories
To pass options to a decorator, write a function that returns a decorator:
function retry(times: number) { return function <This, Args extends unknown[], R>( target: (this: This, ...args: Args) => R, _context: ClassMethodDecoratorContext, ) { return function (this: This, ...args: Args): R { for (let attempt = 1; ; attempt++) { try { return target.apply(this, args); } catch (err) { if (attempt >= times) throw err; console.log(`attempt ${attempt} failed, retrying`); } } }; }; } let calls = 0; class Api { @retry(3) fetchFlaky() { calls++; if (calls < 3) throw new Error("network"); return "data"; } } console.log(new Api().fetchFlaky());
attempt 1 failed, retrying attempt 2 failed, retrying data
Class decorators
A class decorator receives the class itself. A common use is registering classes in a lookup table:
const registry = new Map<string, new () => object>(); function register(name: string) { return function (target: new () => object, _context: ClassDecoratorContext) { registry.set(name, target); }; } @register("csv") class CsvExporter {} @register("json") class JsonExporter {} console.log([...registry.keys()]);
[ 'csv', 'json' ]
Field decorators and addInitializer
Field decorators can transform a field's initial value, and any decorator can schedule code to run when an instance is created via context.addInitializer:
function uppercase(_target: undefined, _context: ClassFieldDecoratorContext) { return (initial: string) => initial.toUpperCase(); } function bound<This, T extends (this: This, ...args: any[]) => any>( _target: T, context: ClassMethodDecoratorContext<This, T>, ) { context.addInitializer(function (this: This) { const self = this as Record<string | symbol, Function>; self[context.name] = self[context.name].bind(this); }); } class Badge { @uppercase label = "beta"; @bound describe() { return `badge: ${this.label}`; } } const { describe } = new Badge(); console.log(describe());
badge: BETA
Without @bound, pulling describe off the object would lose this, and the call would crash.
Standard vs "experimental" decorators
For years, TypeScript supported an older, different decorator design behind the experimentalDecorators compiler flag. Angular, NestJS, TypeORM and MobX (in older versions) were built on it, and you'll see it in many codebases. The two systems aren't compatible:
| Standard (TS 5.0+) | Legacy (experimentalDecorators) | |
|---|---|---|
| Signature | (value, context) | (target, propertyKey, descriptor) |
| Parameter decorators | Not supported | Supported (used by Angular/NestJS DI) |
emitDecoratorMetadata | Not supported | Supported |
| Status | On track to become standard JavaScript | TypeScript-specific |
Use whichever your framework expects. For new code without a framework requirement, use the standard ones — no compiler flag needed.
When to use decorators
Decorators are excellent for cross-cutting concerns that would otherwise be copy-pasted into many methods: logging, timing, caching, authorization checks, retries. They hide behaviour, though, so keep each decorator small, well-named and obvious in what it does.