Skip to content

Decorators

A decorator is a function that wraps or observes a class or a class member, letting you add behavior declaratively with @name syntax. There are two incompatible designs, and it matters which you mean:

  • Legacy decorators — the old, experimental design enabled by experimentalDecorators in tsconfig. Used heavily by older Angular and NestJS. Different signatures, relies on reflect-metadata.
  • Stage-3 decorators — the standardized ECMAScript design, supported natively since TypeScript 5.0 with no special flag. This is the one to learn for new code.

This lesson covers stage-3 decorators.

A method decorator receives the original method and a context object, and returns a replacement (or nothing). Here is a @logged decorator that wraps a method to log its calls:

function logged(originalMethod: any, context: ClassMethodDecoratorContext) {
const name = String(context.name);
function replacement(this: any, ...args: any[]) {
console.log(`calling ${name}`);
return originalMethod.call(this, ...args);
}
return replacement;
}
class Service {
@logged
fetch(id: number) {
return `item ${id}`;
}
}
new Service().fetch(1); // logs "calling fetch", then returns "item 1"

The context object is the heart of the new design. It tells the decorator what it is decorating — context.kind ("method", "field", "getter", …), context.name, context.static, context.private, and an addInitializer hook for setup work.

Decorator kindReceivesTypical use
Classthe class, contextregister it, wrap the constructor
Methodthe method, contextwrap/replace behavior (logging, timing)
Accessorthe getter/setter, contextintercept reads/writes
Fieldundefined, contextreturn an initializer to transform the initial value

A field decorator is a little different — it does not receive a value to wrap; it can return an initializer function that transforms the field’s initial value when an instance is created.

To make a decorator configurable, write a function that returns a decorator — a decorator factory. The @name(args) call runs the factory, and its return value is the actual decorator.

function logged(label: string) { // factory: takes config
return function (originalMethod: any, context: ClassMethodDecoratorContext) {
return function (this: any, ...args: any[]) {
console.log(`[${label}] ${String(context.name)}`);
return originalMethod.call(this, ...args);
};
};
}
class Api {
@logged("http")
get() { /* ... */ }
}
Which decorator design should new TypeScript code use?
What does the second argument to a stage-3 decorator (the context) provide?
What is a decorator factory?
How does a field decorator differ from a method decorator?