Decorators

Wraps a class, method, field or accessor with a function that runs when the class is defined.

Signature
// standard (Stage 3) decorators — TypeScript 5.0+, no flag required
function dec(value: Input, context: DecoratorContext): Output | void;

@dec
class C {
  @dec method(): void { }
  @dec field = 0;
  @dec accessor prop = 0;
  @dec get value(): number { return 0; }
}

// legacy, behind "experimentalDecorators": true
function legacy(target: any, propertyKey: string, descriptor: PropertyDescriptor): void;
  • Added in TypeScript 5.0

Parameters

ParameterTypeDescription
$valueInputThe thing being decorated: the original function for a method, getter or setter; the constructor for a class; undefined for a plain field; a { get, set } target for an auto-accessor.
$contextClassDecoratorContext | ClassMethodDecoratorContext | ClassFieldDecoratorContext | ClassGetterDecoratorContext | ClassSetterDecoratorContext | ClassAccessorDecoratorContextMetadata about the decorated declaration: kind, name, static, private, access, and addInitializer for hooking into construction. The exact type depends on what is being decorated.
$experimentalDecoratorsoptionalboolean= falseCompiler option selecting the legacy Stage 2 decorators instead, whose signature is (target, propertyKey, descriptor).
$emitDecoratorMetadataoptionalboolean= falseLegacy-only option that emits design:type metadata for reflect-metadata. It is not compatible with standard decorators.

Return value

Output | voidA replacement for the decorated value — a wrapped method, a new class, an initializer for a field, or a { get, set } pair for an auto-accessor. Returning nothing leaves the original in place.

Errors

Applying a decorator to a parameter is an error under standard decorators, and combining them with emitDecoratorMetadata is unsupported. A legacy decorator function passed where a standard one is expected receives a context object instead of a property descriptor and fails at runtime.

Changelog

  • PHP 1.5Experimental Stage 2 decorators added behind --experimentalDecorators, with the (target, propertyKey, descriptor) signature and optional --emitDecoratorMetadata.
  • PHP 5.0Standard Stage 3 decorators implemented and enabled without any flag. The new proposal is not compatible with --emitDecoratorMetadata and does not allow decorating parameters.

A decorator is a function applied with @ to a class or one of its members. It runs once, when the class is defined, and whatever it returns replaces the thing it decorated.

Two incompatible systems

TypeScript 5.0 ships the Stage 3 standard decorators, enabled by default with no flag. Their signature is (value, context): the original value, plus a context object carrying kind, name, static, private, access and addInitializer. The older Stage 2 form still exists behind experimentalDecorators and takes (target, propertyKey, descriptor). They are not interchangeable, and a project can only use one.

What trips people up

Standard decorators cannot decorate parameters and do not work with emitDecoratorMetadata. That rules them out for Angular, NestJS and TypeORM, which read design:type metadata through reflect-metadata to resolve constructor injection — those frameworks still require the experimental flag. Also remember that a decorator on a plain field only sees the initial value; to intercept later writes you need an accessor declaration. Note that this page's runner strips types without applying @, so the example below applies the decorator by hand exactly as the runtime would.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Wrap a method with logging, timing or retry behaviour without editing its body.
  • Register a class in a plugin or route table at the moment it is defined.
  • Validate or normalise every write to an auto-accessor property.
  • Add per-instance setup with context.addInitializer, such as binding a method to its instance.

Frequently asked questions

Do I still need experimentalDecorators in TypeScript 5?

Only if you depend on the legacy behaviour — parameter decorators or emitDecoratorMetadata — which is the case for Angular, NestJS and TypeORM. Standard Stage 3 decorators work with no flag at all, but the two systems are mutually exclusive within a project.

What is the difference between standard and experimental decorators?

Standard decorators take (value, context) and return a replacement, while experimental ones take (target, propertyKey, descriptor) and mutate a property descriptor. Standard decorators also drop parameter decorators and emitDecoratorMetadata, and they run under a defined ECMAScript semantics rather than a TypeScript-specific one.

Why does my decorator not see assignments to a property?

Because a decorator on a plain class field only receives the initializer and can only transform the initial value. Declare the property with `accessor` so the decorator receives a get/set pair and can intercept every read and write.

More examples

The same decorator with real @ syntax

Compiled by tsc 5.0 or later with no flags, this prints "bound add" when the instance is constructed, then "enter add(2, 3)", "exit add -> 5" and "result: 5". addInitializer runs per instance during construction, which is the standard hook for binding a method to its instance or registering it somewhere.

Example · typescript
function logged<T, A extends any[], R>(
  original: (this: T, ...args: A) => R,
  context: ClassMethodDecoratorContext<T, (this: T, ...args: A) => R>
) {
  const name = String(context.name);

  context.addInitializer(function (this: T) {
    console.log(`bound ${name}`);
  });

  return function (this: T, ...args: A): R {
    console.log(`enter ${name}(${args.join(", ")})`);
    const result = original.call(this, ...args);
    console.log(`exit  ${name} -> ${result}`);
    return result;
  };
}

class Calc {
  @logged add(a: number, b: number): number { return a + b; }
}

console.log("result:", new Calc().add(2, 3));

Legacy decorators, and why frameworks still need them

Prints "UserService needs [ 'Database', 'Logger' ]" then "function". The container learns the constructor's parameter types purely from emitted metadata, which is how Angular, NestJS and TypeORM resolve dependencies. Standard decorators emit no such metadata and cannot decorate parameters at all, so a project doing this must stay on experimentalDecorators.

Example · typescript
// tsconfig.json:
// { "compilerOptions": {
//     "experimentalDecorators": true,
//     "emitDecoratorMetadata": true } }
import "reflect-metadata";

function Injectable(): ClassDecorator {
  return (target) => {
    const deps = Reflect.getMetadata("design:paramtypes", target) ?? [];
    console.log(target.name, "needs", deps.map((d: any) => d.name));
  };
}

class Database {}
class Logger {}

@Injectable()
class UserService {
  constructor(private db: Database, private log: Logger) {}
}

console.log(typeof UserService);

Discussion

  • Be the first to comment on this lesson.