Reading — step 1 of 7
Learn
Decorators add metadata or behavior to classes, methods, properties, and parameters at definition time. After years as a stage-2 proposal, decorators stabilized as stage-3 in 2022 and TypeScript 5.0 ships the new spec-compliant form.
Note: Older Angular/NestJS code uses the LEGACY decorator syntax (TS 4.x with experimentalDecorators: true). Modern code in TS 5.0+ uses the STAGE-3 form. They differ.
Stage-3 decorators (TS 5.0+)
A class method decorator:
function logged(
target: (this: any, ...args: any[]) => any,
context: ClassMethodDecoratorContext
): typeof target {
return function (...args: any[]) {
console.log(`calling ${String(context.name)} with`, args);
const result = target.apply(this, args);
console.log(`returned`, result);
return result;
};
}
class Calculator {
@logged
add(a: number, b: number): number {
return a + b;
}
}
new Calculator().add(2, 3);
// calling add with [ 2, 3 ]
// returned 5
The decorator function receives:
target— the original methodcontext— metadata about the decoration (kind, name, addInitializer, etc.)
Returns a replacement method (or undefined to keep the original).
Class decorator
function sealed<T extends new (...args: any[]) => any>(
target: T,
_context: ClassDecoratorContext
): T {
Object.seal(target);
Object.seal(target.prototype);
return target;
}
@sealed
class Frozen { /* ... */ }
Class decorators receive the constructor and can return a replacement (or modify in place).
Field decorator
function lowercase(
_value: undefined,
context: ClassFieldDecoratorContext
): (this: any, value: string) => string {
return function (value: string) {
return value.toLowerCase();
};
}
class Email {
@lowercase
address: string;
constructor(addr: string) { this.address = addr; }
}
new Email('[email protected]').address; // '[email protected]'
Field decorators return an INITIALIZER function that's called with the assigned value and returns the actual stored value.
Decorator factories — adding parameters
For decorators that take config:
function min(limit: number) {
return function(
target: any,
context: ClassFieldDecoratorContext
) {
return function(value: number) {
if (value < limit) throw new RangeError(`${String(context.name)} < ${limit}`);
return value;
};
};
}
class Order {
@min(0)
quantity: number;
}
The outer function takes args; the inner is the actual decorator.
context.addInitializer
Lets a decorator register code to run once per instance, after construction:
function bound<T extends (...args: any[]) => any>(
method: T,
context: ClassMethodDecoratorContext
) {
context.addInitializer(function() {
(this as any)[context.name] = method.bind(this);
});
}
class Counter {
count = 0;
@bound
increment() {
this.count++;
}
}
const c = new Counter();
const inc = c.increment; // detached
inc(); // still updates c.count — bound this
Useful for auto-binding methods (no more .bind(this) in constructors).
Legacy vs stage-3 — quick reference
Legacy (experimentalDecorators) | Stage-3 (TS 5.0+) | |
|---|---|---|
| Method decorator signature | (target, key, descriptor) | (method, context) |
| Class decorator signature | (constructor) | (constructor, context) |
| Property decorator | Yes (no value access) | Replaced by accessor / field decorators |
| Parameter decorator | Yes | NOT in stage-3 (yet) |
reflect-metadata integration | Common | Not yet — stage-3 metadata proposal in flight |
Most framework code today (NestJS, Angular, TypeORM) is still on the legacy form — they'll migrate as the new spec settles.
When to use decorators
Good fits:
- Cross-cutting concerns: logging, validation, caching
- Framework hooks: dependency injection, route registration, ORM mapping
- Method binding (
@bound) - Memoization
Bad fits:
- Anything you could express with composition or higher-order functions
- Hot-path code (decorators add overhead per call)
- Anything that should be visible in source — decorators hide behavior
Decorators feel magical. That's a strength for frameworks; a risk for application code.
Common mistakes
- Mixing legacy and stage-3 decorators in one project — the syntax overlaps but the semantics differ. Pick one.
- Forgetting that legacy decorators need
experimentalDecorators: truein tsconfig. - Assuming reflect-metadata works with stage-3 — it doesn't (yet). The stage-3 metadata proposal is separate.
- Side effects in decorators — they run at class-definition time, BEFORE main(). Be careful.
- Decorators on private fields — they don't see
#namefields. Use regular fields or accessors.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…