Decorators cheatsheet (Stage 3, TS 5+).
Enable
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": false // use Stage 3, not legacy
}
}
Legacy decorators (experimentalDecorators: true) are the old pre-TS 5 syntax used by Angular, NestJS, TypeORM. Stage 3 is the new standardized form.
Class decorator
function logged<T extends new (...a: any[]) => any>(
ctor: T,
ctx: ClassDecoratorContext,
) {
return class extends ctor {
constructor(...args: any[]) {
console.log(`creating ${ctx.name}`);
super(...args);
}
};
}
@logged
class Widget {
constructor(public name: string) {}
}
new Widget("a"); // logs "creating Widget"
Method decorator
function trace<This, Args extends any[], R>(
fn: (this: This, ...args: Args) => R,
ctx: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => R>,
) {
return function (this: This, ...args: Args): R {
console.log(`call ${String(ctx.name)}(${args})`);
return fn.call(this, ...args);
};
}
class Calc {
@trace
add(a: number, b: number) { return a + b; }
}
Field decorator
function required<T>(
_value: undefined,
ctx: ClassFieldDecoratorContext<unknown, T>,
) {
return function (initial: T): T {
if (initial == null) throw new Error(`${String(ctx.name)} required`);
return initial;
};
}
class User {
@required name: string = "";
}
Getter / setter decorator
function lazy<T>(
fn: () => T,
ctx: ClassGetterDecoratorContext,
) {
let cached: T;
let done = false;
return function () {
if (!done) { cached = fn.call(this); done = true; }
return cached;
};
}
class Service {
@lazy
get config() { return loadConfig(); }
}
Decorator with arguments (factory)
function deprecated(reason: string) {
return function <T, A extends any[], R>(
fn: (this: T, ...a: A) => R,
ctx: ClassMethodDecoratorContext,
) {
return function (this: T, ...a: A): R {
console.warn(`${String(ctx.name)} deprecated: ${reason}`);
return fn.call(this, ...a);
};
};
}
class Api {
@deprecated("use v2")
v1() { ... }
}
Context object
interface ClassMethodDecoratorContext<This, V> {
kind: "method";
name: string | symbol;
static: boolean;
private: boolean;
access: { has(o: any): boolean; get(o: any): V };
addInitializer(fn: (this: This) => void): void;
}
addInitializer runs after the class body is set up.
metadata (Stage 3)
function tag(value: string) {
return function (_t: any, ctx: ClassDecoratorContext) {
ctx.metadata.tag = value;
};
}
@tag("my-widget")
class Widget {}
console.log(Widget[Symbol.metadata]?.tag); // "my-widget"
Requires Symbol.metadata polyfill in older runtimes.
Real-world use: validation
function min(n: number) {
return function (_v: undefined, ctx: ClassFieldDecoratorContext<unknown, number>) {
return function (initial: number): number {
if (initial < n) throw new Error(`${String(ctx.name)} < ${n}`);
return initial;
};
};
}
class Product {
@min(0) price: number = 10;
}
Memoization
function memo<T, A extends any[], R>(
fn: (this: T, ...a: A) => R,
ctx: ClassMethodDecoratorContext,
) {
const cache = new WeakMap<object, Map<string, R>>();
return function (this: T, ...a: A): R {
let m = cache.get(this as object);
if (!m) cache.set(this as object, m = new Map());
const key = JSON.stringify(a);
if (!m.has(key)) m.set(key, fn.call(this, ...a));
return m.get(key)!;
};
}
class Expensive {
@memo
compute(x: number): number { return heavy(x); }
}
Legacy vs Stage 3 differences
| Aspect | Legacy (experimental) | Stage 3 |
|---|---|---|
| Method signature | (target, key, desc) | (method, ctx) |
| Parameter decorators | Supported | Removed |
| Metadata | reflect-metadata | Symbol.metadata |
| Property descs | Returns descriptor | Returns wrapper function |
| Frameworks | NestJS, Angular, TypeORM | Newer / migrating |
When to use
Stage 3 decorators are still relatively new (early 2026). Most frameworks (NestJS, Angular, TypeORM, class-validator) still target the legacy form. For:
- New libraries → use Stage 3.
- Existing NestJS / Angular code → stick with legacy.
- Application code → usually plain functions are clearer.
Common mistakes
- Mixing legacy and Stage 3 in one project — pick one.
- Parameter decorators in Stage 3 — not part of the proposal.
- Decorator returning wrong shape — TS will complain.
thisinference in method decorators — annotate with<This>.- Using decorators where a HOC / plain function is simpler.
Read this next
If you want my decorator utility library, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .