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.
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 60TypeScript 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.
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:
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)); // 60Legacy (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.
{
"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.
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 { }