TypeScript Decorators
Parent: TypeScript Expert · researched 2026-06-03T23:22:52.632Z· 8 sources · 7 concepts · skill typescript-decorators
A lang-js-ts hub reference for the two distinct decorator systems TypeScript ships. They share the @expr syntax and nothing else: different semantics, different signatures, mutually incompatible emit.
TypeScript Decorators — Standard (Stage 3) vs Legacy (experimentalDecorators), Metadata & Migration
- A lang-js-ts hub reference for the two distinct decorator systems TypeScript ships. They share the @expr syntax and nothing else: different semantics, different signatures, mutually incompatible emit. The single most important fact: the experimentalDecorators compiler flag toggles the whole semantics - flag absent ⇒ TC39 Stage 3 standard decorators (TS 5.0+); flag present ⇒ legacy experimental decorators (TS 1.5-era proposal). For the type system defer to typescript-expert.md; for advanced type operators typescript-advanced-types.md; for tsconfig typescript-compiler-config.md; for why decorators can't run under Node's strip-only TS execution nodejs-typescript-and-runtime-features.md. [source]
Overview
- Standard decorators are functions called at class-definition time with a uniform (value, context) signature; they can replace the decorated value or hook initializers, and they emit plain ES (no reflect-metadata dependency). Legacy decorators use kind-specific signatures (target, propertyKey, descriptor/parameterIndex), support parameter decorators (which Stage 3 still lacks), and - paired with emitDecoratorMetadata - feed runtime type info to the dependency-injection ecosystem (Angular, NestJS, TypeORM, class-validator). That DI dependency is exactly why those frameworks cannot migrate to standard decorators automatically. [source]
- Version anchors (memorize - they drive "does my TS support X" questions): [source]
1. How TypeScript picks a system (the flag is a whole-semantics switch)
- Before TS 5.0, decorators required "experimentalDecorators": true; there was no standard option, so a decorator without the flag was an error. [source]
- TS 5.0+: experimentalDecorators present/true ⇒ legacy semantics + legacy type-checking + legacy emit (__decorate/__metadata helpers). Absent/false ⇒ standard Stage 3 semantics + emit. [source]
- They are not interoperable. A function written for one signature throws or mis-types under the other. The TS 5.0 notes state the new proposal is "incompatible with --experimentalDecorators… and --emitDecoratorMetadata… and parameter decorators." You pick one system per project (effectively per tsconfig). [source]
2. Standard (Stage 3) decorator model — the `(value, context)` signature
- Every standard decorator is (value, context) => replacement | void. value is the thing being decorated (a method/getter/setter function, the class constructor, the {get,set} pair for an auto-accessor, or undefined for a plain field). context is a per-kind object: [source]
- What each kind may return: [source]
- A non-conforming return throws (e.g. a method decorator returning a non-function). [source]
3. The `accessor` keyword (auto-accessors) — TS 4.9
- accessor x = init de-sugars to a private backing field plus a get/set pair on the prototype. It shipped in TS 4.9 (alongside satisfies), deliberately ahead of decorators, so that a kind:"accessor" decorator has a uniform { get, set } to wrap and an init hook to transform the initial value. Supports static and private modifiers. [source]
4. Decorator factories & composition
- A factory is a function returning a decorator - parameterize behavior: [source]
5. Evaluation order vs application order (standard)
- Two separate orderings - do not conflate them: [source]
- **Decorator expressions are evaluated top-to-bottom, left-to-right** (interspersed with computed property names), and the results stashed. [source]
- **Decorators are applied bottom-to-top** on a single element. In @bound @logged greet(), @logged (innermost/closest to the method) wraps the original first; @bound wraps the result. So the expression logged(...) is evaluated before bound, but logged's decorator runs against the raw method and bound's against logged's output. [source]
- The class decorator runs LAST, after all method and non-static field decorators have been applied (the new class isn't available until then). Static field initializers run after the class decorator. Placement around export is allowed on one side only: @reg export default class {} or export default @reg class {}, never both. [source]
6. Metadata — Stage 3 (`Symbol.metadata`) vs legacy (`reflect-metadata`)
- Stage 3 (TS 5.2+, proposal-decorator-metadata): context.metadata is a plain object shared by all decorators on one class. Decorators write into it; after the class is defined it's exposed as TheClass[Symbol.metadata]. No external library, no type reflection - you record what you choose. [source]
- Legacy (emitDecoratorMetadata + reflect-metadata): when both experimentalDecorators and emitDecoratorMetadata are on, tsc emits design-time type metadata for decorated declarations, readable via reflect-metadata's Reflect.getMetadata: [source]
- design:type - the type of a property/accessor. [source]
- design:paramtypes - the constructor/method parameter types (the basis of DI auto-wiring). [source]
- design:returntype - a method's return type. [source]
- This is what powers @Injectable()/constructor injection: the framework reads design:paramtypes to know what to inject. Stage 3 has no equivalent - it records no types and has no parameter decorators. [source]
7. Legacy decorator signatures (still in heavy use)
- target = the prototype for instance members, the constructor for static members. [source]
- Legacy evaluation order (distinct from the standard rule in §5) - TS Handbook "Decorator Evaluation": [source]
- Parameter decorators, then Method/Accessor/Property decorators, for each instance member. [source]
- Parameter decorators, then Method/Accessor/Property decorators, for each static member. [source]
- Parameter decorators for the constructor. [source]
- Class decorators for the class. [source]
- (Within one member, expressions evaluate top-to-bottom, functions are called bottom-to-top - same composition rule as standard.) [source]
Tools / Frameworks
- TypeScript 5.0+ - standard decorators by default; experimentalDecorators for legacy. [source]
- reflect-metadata - runtime metadata store for the legacy emitDecoratorMetadata path; the foundation of DI auto-wiring. [source]
- Angular (16+), NestJS (10+), TypeORM (0.3+), class-validator, TypeGraphQL, MikroORM, routing-controllers - all on legacy decorators + emitDecoratorMetadata. They rely on design:paramtypes (DI) and/or parameter decorators, neither of which exists in Stage 3. [source]
- tsx / ts-node / swc / esbuild / Babel - runners/transpilers that can emit either system's helper code (esbuild supports legacy decorators; standard support varies by tool/version). [source]
Methodology
- Pick a system per project. New code with no DI-framework constraint → standard (no flag) - it's ECMAScript-aligned and library-free. Code on Angular/NestJS/TypeORM/class-validator → stay legacy (experimentalDecorators + emitDecoratorMetadata + reflect-metadata). [source]
- For standard decorators, write (value, context), branch on context.kind, guard static/private, and return the correct shape per kind (esp. the field initializer mutator and the accessor {get,set,init} object). [source]
- For metadata, choose by system: Stage 3 context.metadata/Symbol.metadata (TS 5.2+, record-what-you-choose) vs legacy reflect-metadata + design:* (auto type reflection). [source]
- Never mix systems in one compilation; the flag flips global semantics. [source]
Practical Patterns
- Method wrapping (standard): return a replacement fn from a method decorator; use a factory for parameters. [source]
- Auto-bind: addInitializer(function(){ this[name] = this[name].bind(this); }) in a method decorator - no body edit. [source]
- Field transform: field decorator returns (initial) => transformed; value is undefined. [source]
- Mark-and-collect: write member names into context.metadata; read via instance.constructor[Symbol.metadata] (Stage 3 serialization/validation). [source]
- Legacy DI: @Injectable() class decorator + constructor params whose types tsc emits as design:paramtypes; the container reads them with reflect-metadata. [source]
Anti-Patterns
- Reusing a legacy decorator under the standard system (or vice versa). Signatures differ (target, propertyKey, descriptor vs value, context); it throws or mis-types. Convert deliberately. [source]
- Expecting standard decorators to give you parameter metadata / DI. No parameter decorators, no design:paramtypes in Stage 3. Don't try to port a NestJS/Angular DI app to standard decorators expecting injection to keep working. [source]
- Treating a field decorator's value as the field value. It's undefined; transform via the returned (initial) => … mutator. [source]
- Decorating both get and set of a legacy accessor. Apply to the first accessor in document order only - one PropertyDescriptor covers both. [source]
- Returning a legacy class-replacement constructor without preserving the prototype - the runtime won't do it for you. [source]
- Assuming standard decorators run after Node's native type-strip. Decorators aren't type-only syntax - legacy emits __decorate runtime helpers (needs tsc/tsx/swc/Babel); standard needs engine support V8 hasn't shipped. So .ts with decorators won't run under Node's strip-only path. (Deep mechanics → nodejs-typescript-and-runtime-features.md.) [source]
- Enabling emitDecoratorMetadata without experimentalDecorators - it's legacy-only and has no effect in the standard system. [source]
Troubleshooting
- Decorator "not callable" / wrong-arity errors after a TS 5.0 upgrade → you removed experimentalDecorators and your decorators are legacy-shaped. Re-add the flag or rewrite to (value, context). [source]
- DI stops resolving / Cannot resolve dependencies in NestJS/Angular → experimentalDecorators or emitDecoratorMetadata got turned off, or import "reflect-metadata" is missing from the entrypoint. Restore all three. [source]
- Symbol.metadata is undefined at runtime → missing polyfill (Symbol.metadata ??= Symbol("Symbol.metadata")) and/or lib lacks esnext.decorators; needs TS 5.2+ and target ≤ es2022. [source]
- context.metadata is undefined → TS < 5.2, or you're on the legacy system (legacy decorators have no context). [source]
- A parameter decorator "doesn't exist" under standard decorators → correct; Stage 3 has none. Keep that file on legacy or move the concern to a method/class decorator. [source]
- Property decorator return ignored (legacy) → by design; property decorators can only observe, not modify. Use a method/accessor decorator or accessor + standard. [source]
References
- TypeScript 5.0 - Decorators: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html [source]
- TypeScript 5.2 - Decorator Metadata: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html [source]
- TypeScript 4.9 - Auto-Accessors in Classes: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html [source]
- TS Handbook - Decorators (legacy/experimentalDecorators): https://www.typescriptlang.org/docs/handbook/decorators.html [source]
- TSConfig - emitDecoratorMetadata: https://www.typescriptlang.org/tsconfig/emitDecoratorMetadata.html [source]
- TC39 - proposal-decorators (Stage 3): https://github.com/tc39/proposal-decorators [source]
- TC39 - proposal-decorator-metadata: https://github.com/tc39/proposal-decorator-metadata [source]
- TypeORM #10869 (legacy→standard decorator migration discussion): https://github.com/typeorm/typeorm/issues/10869 [source]
Children
- No children recorded.