← thecodex.expert · The Codex Family of Knowledge
TypeScript

Decorators

Decorator code written before 2023 will not run under TypeScript’s modern default — the two decorator implementations use genuinely different, incompatible function signatures.

TypeScript 6.0 Two incompatible versions Last verified:
Where this bites you

A decorator runs once, when the class is defined — not once per object. Any state you create in the decorator body, like a cache, is therefore shared by every instance of the class. A memoizing decorator written that way hands one object's cached result to another object with different data. It is fully typed, --strict reports nothing, and the numbers are simply wrong.

TypeScriptmemoize_bug.ts
function memoize<This, Args extends any[], Return>(
    target: (this: This, ...args: Args) => Return,
    context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
    const cache = new Map<string, Return>();   // created ONCE, when the class is defined
    return function (this: This, ...args: Args): Return {
        const key = JSON.stringify(args);
        if (cache.has(key)) return cache.get(key)!;
        const result = target.call(this, ...args);
        cache.set(key, result);
        return result;
    };
}

class PriceList {
    constructor(public rate: number) {}

    @memoize
    price(qty: number): number {
        return qty * this.rate;
    }
}

console.log(new PriceList(10).price(3));   // 30
console.log(new PriceList(20).price(3));   // 30   <- should be 60
Canonical Definition

TypeScript 5.0 (March 2023) shipped support for the TC39 Stage 3 ECMAScript decorators proposal as the new default — no compiler flag required. This replaced the older, pre-standard "experimental decorators" TypeScript had supported since 2015 (requiring experimentalDecorators: true), which some established frameworks (Angular, NestJS, TypeORM) still rely on as of 2026 during their own migration. The two are genuinely incompatible: they use different function signatures, and modern Stage 3 decorators notably dropped parameter decorators, which the legacy dependency-injection-heavy frameworks depended on heavily.

Modern (Stage 3) decorators: the default since TypeScript 5.0

A modern method decorator receives the original method plus a typed context object (ClassMethodDecoratorContext, etc.) and returns a replacement function — no flag needed in tsconfig.json for this style.

TypeScriptmodern_decorator.ts
function logged(target: Function, context: ClassMethodDecoratorContext) {
    const methodName = String(context.name);
    return function (this: any, ...args: any[]) {
        console.log(`Calling ${methodName}`);
        return target.call(this, ...args);
    };
}

class Greeter {
    @logged
    greet(name: string) {
        return `Hello, ${name}`;
    }
}

The logged example above has no state, so it never shows this. Per-instance state has to be looked up through this at call time. A WeakMap keyed on the instance gives each object its own cache and lets it be garbage-collected with the object:

TypeScriptmemoize_fix.ts
function memoize<This extends object, Args extends any[], Return>(
    target: (this: This, ...args: Args) => Return,
    context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
    const caches = new WeakMap<This, Map<string, Return>>();   // one cache per instance
    return function (this: This, ...args: Args): Return {
        let cache = caches.get(this);
        if (!cache) { cache = new Map(); caches.set(this, cache); }
        const key = JSON.stringify(args);
        if (cache.has(key)) return cache.get(key)!;
        const result = target.call(this, ...args);
        cache.set(key, result);
        return result;
    };
}

console.log(new PriceList(10).price(3));   // 30
console.log(new PriceList(20).price(3));   // 60

Legacy (experimental) decorators: still required by some frameworks

These use a different, three-argument signature (target, propertyKey, descriptor) and are what Angular, NestJS, and other established dependency-injection-heavy frameworks still require as of 2026, since they built on parameter decorators — a feature the modern standard doesn't include.

TypeScripttsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,   // opt IN to the legacy, pre-2023 behavior
    "emitDecoratorMetadata": true
  }
}

Class decorators and decorator factories

A decorator factory is a function that RETURNS a decorator, letting a decorator accept its own configuration arguments — the common pattern behind decorators like @Component({...}) in frameworks that take options.

TypeScriptdecorator_factory.ts
function withLabel(label: string) {   // the FACTORY
    return function (value: Function, context: ClassDecoratorContext) {   // the actual decorator
        return class extends (value as any) {
            label = label;
        };
    };
}

@withLabel("greeter-v1")
class Greeter { }

Sources

1
Microsoft. TypeScript Handbook, "Decorators", typescriptlang.org/docs/handbook/decorators.html, and the "Announcing TypeScript 5.0" blog post covering the Stage 3 decorator implementation.