Topic 6.8
Decorators
In one line
A decorator is a function written as @name above a class or one of its members. It runs once when the class is defined and can watch, wrap or replace what it decorates. TypeScript 5.0 supports the standard ECMAScript decorators, and the older experimentalDecorators still powers Angular and NestJS.
Think of it like this
Gift wrapping. You hand over a plain book. The wrapper does not change the book: it adds paper, a ribbon, maybe a card, and the person still gets "the book", only dressed. A decorator wraps a class or a method the same way. The method inside is still the same method; the decorator adds logging, timing, a lock, a default value around it. You put the wrapper on with one line (@logged) instead of editing the book.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Decorator
- A function applied with
@nameto a class or member, run once when the class is defined. - Context object
- The second argument of a standard decorator: describes the member (
kind,name,static) and offersaddInitializer. - Initializer
- Code that runs for a field or for each new instance; decorators can register or replace it.
- Cross-cutting concern
- Behaviour needed in many places that is not the main job, like logging or timing.
- Standard decorators
- The ECMAScript decorator design, supported by TypeScript 5.0 without any flag.
- Legacy decorators
- The older
experimentalDecoratorsdesign with different function signatures. - Metadata
- Extra data about types attached for libraries to read at runtime (
emitDecoratorMetadata, legacy only). - Wrapper
- A new function that calls the original and adds behaviour before or after.
Step by step
01A decorator is a function that gets the thing and a context
@logged above add calls logged(add, context) once, when the class is defined. logged returns a new function that logs, calls the original with target.call(this, ...args), logs the result and returns it. The class now has the wrapped version.
The generics This, Args, Return copy the method's own signature, so add is still (a: number, b: number) => number afterwards.
function logged<This, Args extends unknown[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>,
) {
const name = String(context.name);
return function (this: This, ...args: Args): Return {
console.log("calling", name, "with", args);
const result = target.call(this, ...args);
console.log(name, "returned", result);
return result;
};
}
class Calc {
@logged
add(a: number, b: number) {
return a + b;
}
}02One decorator for each kind of thing
A class decorator receives the class and can seal it or return a subclass. A field decorator returns a function that turns the starting value into the final one. An accessor decorator can replace get, set and init. The context's kind tells them apart.
03Evaluated top down, applied bottom up
@tag("first") above @tag("second"): the factory calls run in reading order, but the *decorators* are applied from the one nearest the member outwards. The console shows apply second before apply first.
04addInitializer: do something for every new object
A method decorator can call context.addInitializer(fn). The function runs for each new instance with this set to it. A @bound decorator uses this to replace the method with a version tied to the instance, so const { click } = new Button() still works (compare the detached-method problem in Topic 6.1).
05The old system looks different
Legacy decorators get (target, key, descriptor) and need "experimentalDecorators": true. Angular and NestJS use them, together with emitDecoratorMetadata. Mixing them up gives a clear error.
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}06A legacy decorator is not a standard decorator
A function with the legacy three parameters can't be used as a standard method decorator. TypeScript explains the arity mismatch.
function legacyLog(target: any, key: string, descriptor: PropertyDescriptor) {}
class Svc {
@legacyLog // error TS1241: The runtime will invoke the decorator with 2 arguments, but the decorator expects 3.
run(n: number) {
return n * 2;
}
}Try it yourself
- 1
Time the call
Change
loggedso it also prints how many arguments were passed (args.length). Predict the first console line before you run. - 2
Clamp a different range
In the Volume example change
@clamp(0, 10)to@clamp(1, 3). What do the two console lines print?3and1: the initial value 5 is clamped byinitto 3 as well, then 50 gives 3 and -3 gives 1. - 3
Swap the order
In the order example swap the two
@taglines. Predict the console lines (the labels swap places).
Code & diagrams
The decorator ran once when the class was defined. After that, every call to add goes through the wrapper. The generics keep the original signature.
Console
(nothing was printed)Expected console output
calling add with [ 2, 3 ]
add returned 5
5The first line is printed as soon as the class is defined, before any object exists. kind is "class" and name is "Config". The decorator returned nothing, so it only modified the class.
Console
(nothing was printed)Expected console output
decorating class Config
dev
truedefaultTo(8080) is a decorator factory: a function that returns the decorator. The returned initializer receives the field's starting value. port has none, so it gets 8080. retries starts at 0, which is not undefined, so it stays 0.
Console
(nothing was printed)Expected console output
decorating port field
decorating retries field
8080 0The accessor keyword gave the decorator a getter and a setter to wrap. Writing 50 stores 10, writing -3 stores 0.
Console
(nothing was printed)Expected console output
10
0The decorators were applied when the class was defined, before run() was ever called. The one nearest the method is applied first.
Console
(nothing was printed)Expected console output
apply second to run
apply first to run
doneCompare Topic 6.1: a detached method used to lose this. The initializer replaced click on each new instance with a bound copy, so the detached call works.
Console
(nothing was printed)Expected console output
OKThis example turns on experimentalDecorators, the option Angular and NestJS rely on. Same idea, different signature: (target, key, descriptor) instead of (value, context). Without the option TypeScript rejects this decorator.
Console
(nothing was printed)Expected console output
legacy call run [ 4 ]
8Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Use a legacy decorator in a standard project
Use the legacy.ts snippet from the walkthrough: a function legacyLog(target, key, descriptor) applied as @legacyLog on a method.
Myth vs fact
Myth
Decorators run every time a method is called.
Fact
The decorator itself runs once, when the class is defined. If it returns a wrapper, the wrapper runs on every call.
Myth
Decorators need experimentalDecorators.
Fact
That flag turns on the old design. Standard decorators (TypeScript 5.0) need no flag, and mixing the two does not work.
Myth
A decorator can see the constructor parameters like Angular's injection does.
Fact
Parameter decorators exist only in the legacy system. Standard decorators cover classes, methods, accessors and fields.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Standard decorators are a stage 3 ECMAScript proposal. Engines don't run
@syntax natively yet, so TypeScript lowers it to helper functions (__esDecorate,__runInitializers) at emit time. That is why the examples here work in the browser, and why your build tool must support the TypeScript emit (or a compatible transform), not just type-stripping. - ▸
Decorating a field can't reach the instance during decoration (
thisis not available), only inside the returned initializer. To change how a field reads and writes, use theaccessorkeyword (Topic 6.6) and an accessor decorator. - ▸
The old and the new system disagree on parameter lists, evaluation details and metadata (
emitDecoratorMetadatais legacy-only). Libraries are migrating at different speeds, so check what each dependency expects before turningexperimentalDecoratorson or off. - ▸
Typing decorators is the part that takes practice. Capture the method's
this, its argument list and its return type with generics (This,Args extends unknown[],Return) so the wrapped method keeps its signature, and choose the context type that matches the kind you decorate.
Remember this
- 1
The problem this solves: Extra behaviour such as logging or checking would otherwise be copied into every method that needs it, which buries the real logic. A decorator is just a function. You apply it by writing
@decoratorNameon the line above a class, method, getter, setter, field oraccessor. It runs once, when the class is defined (not each time an object is created or a method is called). It receives the thing being decorated and a context object, and it may return a replacement. So@loggedaboveaddmeans roughlyadd = logged(add, context). - 2
TypeScript 5.0 implemented the standard ECMAScript decorators (an ECMAScript proposal that was at stage 3 when TypeScript 5.0 shipped it; engines do not run the
@syntax natively, so check current support before relying on it). TypeScript lowers them into ordinary JavaScript while compiling, which is why they run in the live editor even though the browser has no decorator syntax. You don't need any compiler flag. This is a different system from the old one (below), with different function signatures. - 3
The context object describes what is being decorated. Its
kindis"class","method","getter","setter","field"or"accessor";nameis the member's name;staticandprivatesay what sort of member it is;accesslets you read and write the value;metadatais a shared object for the class; andaddInitializer(fn)registers code to run later (when the class is finished, or for each new instance). TypeScript gives each kind a context type:ClassMethodDecoratorContext,ClassFieldDecoratorContext,ClassDecoratorContext,ClassAccessorDecoratorContextand so on, which you write in your decorator's parameter list so the checker knows what's allowed. - 4
What each kind receives and may return. A method decorator gets the original function and may return a replacement function (the
loggedexample wraps it). A class decorator gets the class and may return a subclass (or nothing, as withsealed, which only modifies it). A field decorator getsundefined(fields have no value yet) and may return an initializer function that receives the starting value and returns the final one. An accessor decorator gets the originalgetandsetand may return a newget,setandinit; this is why theaccessorkeyword (Topic 6.6) exists: a plain field has no getter or setter to wrap. - 5
Order of events. With several decorators on one member, they are *evaluated* top to bottom (the expressions after
@) but *applied* bottom to top, like wrapping a gift: the paper goes on before the ribbon. In the example,@tag("second")runs first and@tag("first")wraps last. For a whole class, member decorators run first, then the class decorator. - 6
Writing a typed method decorator takes generics, because the decorator should work for any method:
<This, Args extends unknown[], Return>(target: (this: This, ...args: Args) => Return, context: ClassMethodDecoratorContext<This, ...>). The type parameters capture the method'sthis, its argument list and its return type, so the wrapper has the same signature as the original. Copy this shape; it's the same pattern every logging or timing decorator uses. - 7
Legacy decorators. Before the standard existed, TypeScript had
experimentalDecorators(a compiler option), with a different shape: a method decorator gets(target, key, descriptor), there are parameter decorators (used for constructor injection), andemitDecoratorMetadataadds runtime type information. Angular and NestJS are built on this system and keep requiring those options in theirtsconfig(check your framework's current guide, since they are moving over time). The two systems are incompatible: a legacy-style decorator function used in a project withoutexperimentalDecoratorsfails to compile (TS1241, with the lineThe runtime will invoke the decorator with 2 arguments, but the decorator expects 3.). Standard decorators have no parameter decorators. - 8
Use decorators for cross-cutting concerns: logging, timing, caching, access checks, binding
this, registering a class with a framework. Do not hide business logic in them, because magic that changes behaviour from a distance is hard to follow and debug. Remember they run at definition time, so any side effect in the decorator body happens when the file loads.
Explain it without notes
What is a decorator, and when does it run?
What does the context object contain and what is addInitializer for?
How do standard decorators differ from experimentalDecorators?
In which order are several decorators on one method evaluated and applied?
Practice
Write a method decorator timed that logs "done" after the method returns, keeping the method's signature. Apply it to a method that returns 42 and print the result.
Write a class decorator registered that pushes the class name into a names: string[] array when the class is defined. Apply it to two classes and print the array.
Trade-offs
- ↔
Decorators remove repeated plumbing (logging, caching, registration) from the code that matters, but they change behaviour out of sight, so overuse makes code hard to follow.
- ↔
Standard decorators are the future and need no flags; legacy ones are what many frameworks still demand today. You may need to understand both.
Done when you can
I can write a typed method decorator that wraps a method with generics.
I can say when a decorator runs and what the wrapper it returns does.
I can name what a class, method, field and accessor decorator receive and may return.
I can explain the order of several decorators on one member.
I can tell standard decorators from
experimentalDecoratorsand explainTS1241.