Capítulo 19 de 36

Chapter 19: Decorators (Legacy/Experimental, Stage 2)

Core Idea

Decorators (@expression) are a meta-programming layer for observing, modifying, or replacing classes and their members at definition time — this reference page documents the older experimental stage-2 implementation (behind experimentalDecorators), distinct from the newer stage-3 decorators shipped natively since TypeScript 5.0.

Key Concepts

  • Enabling: requires experimentalDecorators: true in tsconfig.json (or --experimentalDecorators on the CLI) for this stage-2 form.
  • Decorator factories: a plain @sealed decorator is just a function; a decorator factory is a function that returns a decorator function, letting you parameterize it — @enumerable(false).
  • Composition order is "function composition" order, not top-to-bottom execution order: with multiple decorators stacked on one declaration, every decorator factory expression evaluates top-to-bottom first, then the resulting decorator functions are called bottom-to-top (closest-to-the-declaration first) — mirroring mathematical (f ∘ g)(x) = f(g(x)).
  • Evaluation order across a class: for each member (instance members, then static members), parameter decorators run before method/accessor/property decorators on that same member; constructor parameter decorators run next; class decorators run last, after every member decorator.
  • Class decorators: receive the class's constructor as their only argument; can observe/mutate it (e.g. Object.seal(constructor) to lock it against further modification) or return a new constructor to replace the class entirely — if you do the latter, you're responsible for preserving the original prototype chain yourself, since the decorator runtime doesn't do it for you. A class decorator that adds a new property (e.g. via a returned subclass) does not update the TypeScript-visible type of the class — the new member exists at runtime but is invisible to the type-checker.
  • Method / accessor decorators: receive (target, propertyKey, descriptor) — target is the prototype (instance member) or constructor (static member), descriptor is the standard JS PropertyDescriptor. Returning a value from the decorator replaces the descriptor. TypeScript disallows decorating both get and set of one accessor pair separately — decorators apply to whichever accessor is declared first, since both share one underlying PropertyDescriptor.
  • Property decorators: receive only (target, propertyKey)no PropertyDescriptor and no way to observe/modify the field's initializer, because there's no mechanism to describe an instance property at prototype-definition time. In practice, property decorators can only record metadata about "a property with this name exists" (e.g. via the reflect-metadata library), not intercept reads/writes.
  • Parameter decorators: receive (target, memberName, parameterIndex) — can only observe that a parameter at a given position exists (e.g. to record "this parameter is required" as metadata); the return value is ignored. Real enforcement (like actually throwing on a missing required argument) has to happen in a paired method decorator that reads the metadata the parameter decorator recorded.
  • emitDecoratorMetadata + reflect-metadata: an additional experimental compiler option that, combined with the reflect-metadata polyfill library, emits design-time type information (design:type etc.) accessible at runtime — enabling patterns like a @validate setter decorator that checks the assigned value's runtime type against the property's declared TypeScript type.

Code Examples

function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class BugReport {
  title: string;
  constructor(t: string) { this.title = t; }
}
  • What it demonstrates: a class decorator that locks a class's constructor and prototype against further runtime modification, without preventing subclassing.
function enumerable(value: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.enumerable = value;
  };
}
class Greeter {
  @enumerable(false)
  greet() { return "hi"; }
}
  • What it demonstrates: a decorator factory (enumerable(false) returns the actual decorator function) mutating a method's property descriptor.

Reference Tables

Decorator kindArguments receivedCan it replace/mutate?
Classconstructoryes — return a new constructor to replace the class
Method / Accessortarget, propertyKey, descriptoryes — return a new descriptor
Propertytarget, propertyKeyno — observation only, no descriptor access
Parametertarget, propertyKey, parameterIndexno — observation only, return value ignored

Key Takeaways

  1. This page describes the experimental stage-2 decorator implementation — check which decorator model a project actually targets (experimentalDecorators vs. native TS 5.0+ decorators) before applying patterns from here.
  2. A class decorator that returns a new constructor changes runtime behavior but never changes the TypeScript-visible type — new members it adds are invisible to the type-checker unless separately declared.
  3. Property and parameter decorators can only observe, not intercept — real validation/enforcement logic has to live in a paired method decorator that reads metadata the observing decorators recorded.
  4. reflect-metadata + emitDecoratorMetadata is what unlocks runtime access to a property's declared TypeScript type — without it, decorators only see what you explicitly pass them.

Connects To

  • Classes — Members & Advanced: the class/method/accessor/property/parameter constructs decorators attach to.
  • Declaration Merging: another meta-programming mechanism for extending class-like declarations, via a different route (merging vs. runtime interception).