A decorator is a function that runs when a class is defined, receiving the thing it is attached to and either recording something about it or replacing it. That is the whole idea. Everything else is detail — including the awkward detail that TypeScript currently has two incompatible decorator systems.
What you are actually looking at
Here is a pipe from the Angular half of the pizza app:
@Pipe({ name: 'money' })
export class MoneyPipe implements PipeTransform {
transform(value: number | null | undefined): string {
return formatMoney(value ?? 0);
}
}@Pipe({ name: 'money' }) is a function call. It returns a decorator, which Angular
applies to MoneyPipe when the module loads, and which records "this class is a pipe
named money" in a metadata table Angular keeps.
Nothing about MoneyPipe changes. It is still a class with one method. What has
happened is that a framework now knows something about it — enough to find it when a
template writes {{ total | money }}.
That is the pattern behind almost every decorator you will meet: attach metadata that a framework reads later.
The four in this codebase
@Component marks a class as a component and supplies its selector, template and
change-detection strategy:
@Component({
selector: 'app-product-card',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [MoneyPipe],@Injectable marks a class as available for injection, and
providedIn: 'root' makes it a singleton for the whole application:
@Injectable({ providedIn: 'root' })
export class AuthService {@Directive marks behaviour attachable to an element somebody else owns:
@Directive({
selector: '[appAutofocus]',
})
export class Autofocus {And @Pipe, above. In every case the class is ordinary and the decorator is
registration.
Writing one
A class decorator is a function taking the constructor:
function logged(constructor: Function) {
console.log(`defined ${constructor.name}`);
}
@logged
class CartService {}
// logs "defined CartService" when the module loads — not when one is constructedTo take arguments, return the decorator from a factory. That is why every Angular decorator has parentheses:
function tag(label: string) {
return function (constructor: Function) {
Reflect.defineMetadata?.('tag', label, constructor);
};
}
@tag('cart')
class CartService {}Method, property, accessor and parameter decorators exist too, each with a different signature. The most useful thing to know is the order: decorators are evaluated top to bottom and applied bottom to top, which matters when two of them wrap the same method.
The metadata trick behind dependency injection
This is the part that looks like magic, and is not.
Frameworks such as NestJS resolve dependencies from a constructor's parameter types:
@Injectable()
class OrderService {
constructor(private readonly repo: OrderRepository) {}
}But types are erased. At runtime there is no OrderRepository in that signature —
so how does the injector know what to supply?
The answer is emitDecoratorMetadata. When a class carries a decorator and that flag
is on, TypeScript emits a side table of the parameter types as runtime values:
Reflect.metadata("design:paramtypes", [OrderRepository])The framework reads design:paramtypes and looks each constructor up. It is the one
place where TypeScript's type information survives into the running program — and it only works for
things that exist at runtime. A parameter typed as an interface emits Object, which is
why DI frameworks make you inject classes or explicit tokens.
Note also that this requires reflect-metadata to be imported once at startup, and
that it is specific to the legacy decorator system.
Two decorator systems
Here is the state of play, which is genuinely confusing and worth stating plainly.
Legacy decorators — enabled with experimentalDecorators. Based on a
proposal that never became a standard. This is what Angular, NestJS and TypeORM use, and it is what
emitDecoratorMetadata works with.
"isolatedModules": true,
"experimentalDecorators": true,Standard decorators — shipped in TypeScript 5.0, implementing the Stage 3
proposal that will become part of JavaScript. Enabled by default, with a different signature: a
class decorator receives (value, context) rather than just the constructor.
They are not compatible, and you cannot use both in one project. emitDecoratorMetadata
does not apply to the standard ones, so frameworks that depend on parameter-type reflection are
still on the legacy flag — which is why an Angular project in 2026 still sets an option called
"experimental".
If you are writing your own decorators in a new project, write standard ones. If you are working in Angular or NestJS, use what the framework uses.
Why the React half has none
Back to the flag from lesson 18:
"erasableSyntaxOnly": true,Decorators are on its banned list, for the reason that runs through this whole track: a decorator has to emit a call. It is not an annotation to be deleted, it is code.
Which is fine, because React needs nothing from them. A component is a function, a hook is a function call, and there is no injector wanting to know a constructor's parameter types. The metadata problem decorators solve does not arise.
Angular is moving away from them too
Worth noticing, because it changes what modern Angular code looks like. The framework is replacing decorator-based declarations with plain functions.
Dependency injection used to be a constructor parameter, resolved from its type. The app uses
inject() instead:
export class Autofocus {
private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);That is an ordinary function call with an explicit type argument. No parameter, no metadata emission, nothing inferred from an erased type — and it works in a field initialiser, which a constructor parameter cannot.
Inputs and outputs have gone the same way. @Input() and @Output()
became functions:
/** `input.required` makes a missing binding a COMPILE error, not an undefined at runtime. */
readonly product = input.required<Product>();
readonly selected = output<Product>();And that comment is the point. input.required<Product>() carries its type as a
real type argument, so the compiler can check every binding. The decorator version could not — it
had to describe the type in a place the template checker had a harder time reaching.
Even the transform is a value rather than a convention:
readonly appAutofocus = input(true, { transform: booleanAttribute });@Component, @Injectable, @Pipe and @Directive
remain, because a class still has to be registered as one of those things and a decorator
is a reasonable way to say so. Everything that was about types has moved to functions with type
arguments.
A standard decorator, end to end
Since the standard form is what new code should use, here is one complete. A method decorator that logs how long a call took:
function timed<This, Args extends unknown[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, typeof target>,
) {
const name = String(context.name);
return function (this: This, ...args: Args): Return {
const start = performance.now();
try {
return target.call(this, ...args);
} finally {
console.log(`${name} took ${(performance.now() - start).toFixed(1)}ms`);
}
};
}class Cart {
@timed
recalculate() { /* … */ }
}Three things distinguish this from the legacy form. It receives the method itself as
target, rather than a property descriptor to mutate. It receives a
context object carrying the member's name, kind and static-ness. And it
returns a replacement, which is how it wraps.
The generics are doing real work: This, Args and Return
are inferred from the method being decorated, so the wrapper has exactly the same signature and
nothing is widened to any. That is the practical improvement over the legacy system,
where a decorator typically returned PropertyDescriptor and the types were lost.
The context object also gives you addInitializer, for work that should happen when
an instance is constructed — which is how you would implement something like auto-binding a method
to its instance.
The shapes you can decorate
| Target | Typical use |
|---|---|
| class | registration — @Component, @Injectable |
| method | wrapping — logging, retries, caching, authorisation |
| getter / setter | the same, around a property access |
| field | transforming the initial value, or registering the field |
| parameter | legacy only — @Inject, @Body |
That last row matters if you are reading NestJS: parameter decorators such as
@Body() and @Param() are not part of the standard proposal, which is
another reason those frameworks remain on experimentalDecorators.
When to write your own
Rarely, in application code. A decorator hides control flow: the reader sees
@Retry(3) and has to go elsewhere to learn what happens. That is a good trade when the
behaviour is genuinely cross-cutting and the alternative is repeating it everywhere — logging,
caching, transactions, authorisation — and a bad one when a higher-order function would read more
plainly.
They are also, structurally, a framework feature. The reason Angular and NestJS use them heavily is that they need a registry of classes and their roles. If you are not building something with a registry, the case is thin.
What to remember
Three things, if the two-systems business has crowded everything else out.
A decorator is a function that runs at definition time. Not when an instance is
created, not when a method is called. @Pipe(...) executes as the module loads, and
whatever it does has happened before your code runs.
Almost all of them are registration. The class is ordinary; the decorator tells a framework it exists and what role it plays. Once you read them that way, Angular and NestJS stop looking magical.
They emit code, which is why they are the fourth item on
erasableSyntaxOnly's banned list alongside enum and parameter properties,
and why a project's decision about that flag decides whether they are available at all.
A practical note for anyone starting a project today. If you are choosing a framework, the
decorator question is settled for you: Angular and NestJS use them heavily, React and Vue do not use
them at all. What you should not do is enable experimentalDecorators in a project that
has no framework requiring it, on the grounds that decorators look tidy. That commits your source to
a non-standard dialect for a syntax preference.
The confusion around decorators is almost entirely historical rather than conceptual. Strip away the two competing systems and the flag names, and what remains is one sentence: a function, called with the thing below it, at the moment that thing is defined. Everything Angular and NestJS do with them is an application of that.
If you want to see it for yourself, put a console.log in a class decorator and load
the module without constructing anything. Watching it fire is worth more than any explanation.
And if you are reading a framework's source to work out what one of its decorators does, look for where the metadata is read rather than where it is written. The decorator itself is almost always three lines; the interesting code is the injector, the router or the template compiler that consults the table afterwards.
Next
TypeScript with React — props, hooks, events, and
the context pattern that removes an | undefined from every consumer.