typescript
TypeScript 5.0 and the decorators that are finally the real ones

TypeScript 5.0 was released on the sixteenth of March, and the headline is decorators. If you write NestJS, you have been using decorators daily for years, so the natural reaction is to wonder what exactly was missing.
The answer is that they are not the same feature, and the distinction is worth twenty minutes because it decides whether you can delete a compiler flag.
Two features with one syntax
What NestJS uses — and Angular, and TypeORM, and every library in that lineage —
are experimental decorators, enabled with experimentalDecorators in
tsconfig.json. They implement a TC39 proposal from around 2015 that never
advanced. TypeScript shipped them anyway because the frameworks needed something,
and the ecosystem built a decade of tooling on a design that was never finished.
What 5.0 shipped is the Stage 3 proposal — the one that is on track to become
part of JavaScript itself. Same @ syntax, different semantics, different shape
of the function you write.
Under the old design a method decorator receives the target, the property key and the property descriptor. Under the new one it receives the value and a context object:
function logged<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>,
) {
return function (this: This, ...args: Args): Return {
console.log(`entering ${String(context.name)}`)
return target.call(this, ...args)
}
}
class Service {
@logged
handle(id: string) {
return id.toUpperCase()
}
}
The context object is the real improvement. It carries the name, the kind,
whether the member is static or private, and an addInitializer hook — and it is
typed, which the old descriptor-based signature never usefully was.
The part that matters in practice
The two cannot be mixed. If experimentalDecorators is on, you get the old
behaviour; turn it off and you get the new one.
There is also no metadata. emitDecoratorMetadata — the flag that makes
TypeScript emit design-time type information for reflect-metadata to read — is
tied to the experimental implementation. That flag is how NestJS resolves a
constructor parameter to a provider without you naming it, and how class-validator
knows a field is a Date. The standard decorators have no equivalent in 5.0.
So for anything built on that ecosystem, the practical answer this March is: nothing changes. Keep both flags on, keep writing decorators the way the framework documents, and wait for the frameworks to move. They will, and it will not be a small release when they do.
Where the new ones are immediately useful is your own code — a @logged, a
@measured, a @retry in a service you own — in a project that does not depend
on decorator metadata. There the standard version is better typed and will
eventually need no compiler support at all.
The rest of the release, which I adopted the same week
Two smaller things I have already used.
const type parameters, which fix an annoyance that has made me write as const
thousands of times:
function tags<const T extends readonly string[]>(values: T): T {
return values
}
const t = tags(['aws', 'terraform']) // readonly ["aws", "terraform"]
Previously that inferred string[] and you had to remember the assertion at every
call site. Now the library author makes the decision once.
And all enums are union enums now, which means a numeric enum finally narrows properly instead of accepting any number that happens to fit.
The module resolution work is the other one worth a look — moduleResolution: bundler describes what bundlers actually do, rather than making you pick between
two Node resolution modes that both describe something else. If you have ever
argued with a tsconfig about file extensions in import paths, that flag is
probably what you wanted.
The bit I find interesting
TypeScript shipped a feature early, the standards process took eight more years, and the result is two incompatible things wearing the same syntax in the same language. Nobody did anything unreasonable at any step — the frameworks needed decorators and could not wait, and TC39 was right to keep working.
But it is a good argument for a rule I try to hold: when you adopt something labelled experimental, the thing to write down is not whether it works. It is what you will do when the real version arrives and does not match.
Written by
Deyan Peev
Founding Engineer · Sofia, Bulgaria


