Decorators have been usable in TypeScript behind a flag since roughly forever, but the ones you get by default in TypeScript 5.0+ are not the same feature. The old experimentalDecorators implementation tracked an early, stalled TC39 proposal. The new decorators — on by default, no flag needed — implement the actual Stage 3 proposal that shipped in TypeScript 5.0 and is now landing natively in V8 and other engines. They look similar at the call site (@logged above a method still reads the same way) but the function you write underneath has a different signature, different capabilities, and different gaps. If you copy a decorator recipe from a five-year-old blog post or from reflect-metadata-based library docs, there's a good chance it won't compile under the new model without changes.
Old decorators vs. the new standard
Under experimentalDecorators, a method decorator received three arguments: the target (the class prototype), the property key, and a PropertyDescriptor. You mutated the descriptor in place or returned a new one. Class decorators received the constructor itself and could return a replacement constructor. This shape was never standardized — it was a TypeScript-specific compile target modeled loosely on an early decorators draft, and it required emitDecoratorMetadata plus the reflect-metadata polyfill if you wanted runtime access to parameter types, which is how libraries like older versions of Angular and NestJS did dependency injection by type.
The new Stage 3 decorators, standard as of TypeScript 5.0 with no compiler flag required, work differently. Every decorator receives the thing it's decorating as the first argument and a context object as the second. The context object tells you the kind ("method", "field", "class", "getter", "setter", "accessor"), the name, whether it's static or private, and gives you hooks like addInitializer for code that should run when an instance is constructed. Critically, there is no built-in metadata reflection — emitDecoratorMetadata and reflect-metadata don't apply to this model at all. If a library's DI container depended on reading parameter types at runtime via Reflect.getMetadata('design:paramtypes', ...), that trick doesn't carry over; the new proposal deliberately left type-directed metadata out of scope; a separate proposal (Symbol.metadata) exists to fill part of that gap, but it's not the same mechanism and adoption is still thin.
You cannot mix the two models in one file by accident, but you can absolutely end up on the wrong one project-wide. If tsconfig.json has "experimentalDecorators": true, you're compiling against the old, non-standard shape — TypeScript 5.0's default (flag omitted or explicitly false) gives you the new one. Check this before debugging a decorator that "isn't receiving the right arguments"; it's usually just the wrong mode.
Method decorators: @logged
A method decorator under the new model is a function that takes the original method and a context, and returns either nothing (keep the original) or a replacement function. Here's a real one — logging entry, exit, and duration for a method call, which is the decorator every codebase eventually writes a version of by hand:
function loggedMethod(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`[${methodName}] called with`, args);
const start = performance.now();
const result = originalMethod.call(this, ...args);
const ms = (performance.now() - start).toFixed(2);
console.log(`[${methodName}] returned in ${ms}ms`);
return result;
}
return replacementMethod;
}
class InvoiceService {
@loggedMethod
calculateTotal(lines: number[]) {
return lines.reduce((sum, n) => sum + n, 0);
}
}
Notice what changed from the old shape: no descriptor to mutate, no prototype passed in — just the function and a context object. context.name gives you the method name without digging it out of a property key, and because replacementMethod is a plain function (not an arrow function), this still binds correctly when TypeScript calls it as a method.
Field decorators and class decorators
Field (property) decorators receive the initial value and a context, and can return a function that computes the actual value to assign — useful for lazy defaults or validation on assignment. Class decorators receive the class itself and a context with kind: "class", and can return a new class that extends the original, which is how you'd add shared behavior across every instance without touching each method.
A good field-decorator use case is auto-binding, replacing the familiar this.handleClick = this.handleClick.bind(this) in a constructor:
function bound(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = context.name;
context.addInitializer(function (this: any) {
this[methodName] = this[methodName].bind(this);
});
// return nothing: keep the original method, just add the initializer
}
class Button {
label = "Save";
@bound
handleClick() {
console.log(`clicked: ${this.label}`);
}
}
const { handleClick } = new Button();
handleClick(); // "this" is still the Button instance, not undefined
context.addInitializer is the piece that didn't exist in the old model at all — it queues a callback that runs once per instance, right when the constructor runs, which is exactly the timing you need for binding this without re-running the bind on every method call the way a wrapping decorator would.
What you lose without metadata reflection
The practical friction most teams hit isn't the decorator syntax — it's everything that used to lean on emitDecoratorMetadata. Type-based dependency injection (resolve a constructor parameter by its declared class), some ORMs' automatic column-type inference from property types, and validation libraries that read a property's TypeScript type at runtime all depended on that metadata being emitted and readable via reflect-metadata. None of that exists under the standard decorators model by default. Libraries in this space have either stayed on experimentalDecorators deliberately, shipped a parallel API that takes explicit type arguments instead of inferring them, or adopted the newer Symbol.metadata proposal where it's supported — but "swap the flag and everything still works" is not a safe assumption for any codebase using decorator-based DI or ORMs.
Before removing experimentalDecorators from an existing project, grep for reflect-metadata imports and anything that calls Reflect.getMetadata or Reflect.defineMetadata. If you find them, the library you're using needs an update path to the new model, not just a tsconfig change.
Wrapping up
The standard decorators in TypeScript 5.0 are a real TC39 feature, not a TypeScript-only convenience, and that's the whole point of the change — they'll work the same way once engines implement them natively, with no compile step required at all. The tradeoff is that the new signature (subject, then context, rather than target/key/descriptor) and the missing metadata reflection mean decorator code and decorator-dependent libraries written for the old flag don't just carry over. If you're starting a new project, use the default standard decorators and don't reach for experimentalDecorators out of habit. If you're maintaining one that leans on decorator-based DI or ORM metadata, budget time to check whether your dependencies have already made the jump before you do.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.