The New JavaScript Decorators Are Not What You Remember
Standard decorators landed in TypeScript 5: no descriptors, a context object, and auto-accessors. Here's the API you'll actually use.
If you learned decorators from NestJS, TypeORM, or a 2019 TypeScript tutorial, unlearn them. The decorators that actually made it into ECMAScript — and into TypeScript 5 — have a completely different API. Same @syntax, entirely new machinery underneath.
The old experimental proposal had you wrestling with PropertyDescriptor objects and generated __decorate helpers. The standard hands you the method itself, plus a context object full of hooks. It's simpler, and it does things the old API literally couldn't.
Why this matters
Decorators saturate backend TypeScript: dependency injection, ORM entities, route binding, validation. Nearly all of it targets the legacy "experimental" flavor, which is now a dead end.
The standard flavor is where the ecosystem is moving. Chromium ships it natively, TypeScript 5 compiles it for everything else, and Babel matches it. When you upgrade a framework or write a library, you'll meet this API — and mixing the two flavors fails in ways that are genuinely hard to debug.
How it works
A decorator is now a plain function taking two arguments: the decorated value and a context object.
The context tells you what you're decorating via kind — method, getter, field, accessor, or class — plus a name. To change behavior, return a replacement function. Return nothing and the original survives untouched.
addInitializer queues a callback that runs during construction, which is the right place to bind methods or register instances. You never touch a descriptor again.
The genuinely new trick is the accessor keyword. Write accessor token = "" and the runtime generates a getter/setter pair backed by a private slot. Your decorator receives that { get, set } pair and can wrap it — clean interception of writes, no Object.defineProperty gymnastics.
Factories survive unchanged: @trace(verbose) is still a function returning a decorator.
Where this helps
- Wrapping service or repository methods with timing, retries, or auth checks without touching a single call site.
- Write-time validation via accessor decorators: reject empty config values, clamp numbers, redact secrets on read.
- Registration: an initializer that pushes instances into a registry, powering lightweight DI or event-bus wiring.
- Library code that runs natively in Chromium and compiles identically through TS or Babel — one implementation, no flavor matrix.
Watch out
Legacy and standard decorators are incompatible in both directions. NestJS-style metadata doesn't exist under the new semantics. Check which flavor a package targets before stacking its decorators on standard ones.
In TypeScript, do not enable experimentalDecorators — that flag silently switches the compiler back to legacy behavior.
Decoration runs once, at class definition. You can't read instance state in a decorator; use addInitializer or the wrapper's closure instead.
Native support outside Chromium is uneven. Compile with TypeScript 5 or Babel if you ship broadly.
Try it yourself
// trace.ts — TypeScript 5+, target ES2022, no experimentalDecorators flag
// run: npx tsc trace.ts && node trace.js
function trace(method: Function, ctx: ClassMethodDecoratorContext) {
const name = String(ctx.name);
return function (this: unknown, ...args: unknown[]) {
console.time(name);
const out = method.apply(this, args);
console.timeEnd(name);
return out;
};
}
class Invoices {
@trace
totalFor(user: string): number {
return 42; // pretend this hits the DB
}
}
new Invoices().totalFor("ada");
// totalFor: 0.031msTL;DR
- What changed: ECMAScript standardized decorators; TypeScript 5 and Chromium ship a descriptor-free API built around a context object.
- Why it matters: the legacy decorator ecosystem is frozen; new framework and library code will use the standard flavor.
- Try today: write the
@tracemethod decorator above and notice you never touched a PropertyDescriptor.