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

AspectLegacy (experimental)Stage 3
Method signature(target, key, desc)(method, ctx)
Parameter decoratorsSupportedRemoved
Metadatareflect-metadataSymbol.metadata
Property descsReturns descriptorReturns wrapper function
FrameworksNestJS, Angular, TypeORMNewer / 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.
  • this inference 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 .