Standard Decorators (TS 5)
The TC39 decorators shipped in TypeScript 5 β annotations that wrap classes and members.
TypeScript 5.0 adopted the standard (Stage 3) decorators β no more experimentalDecorators flag for this modern form. A decorator is a function prefixed with @ that runs when the class is defined, letting you observe or replace what it decorates.
The modern signature
Each decorator receives the target and a context object describing what is being decorated (its kind, name, and hooks like addInitializer):
function logged(
target: Function,
context: ClassMethodDecoratorContext
) {
return function (this: any, ...args: any[]) {
console.log(`calling ${String(context.name)}`);
return target.apply(this, args);
};
}
class Api {
@logged
fetch(url: string) { return `GET ${url}`; }
}Method decorators return a replacement function; class decorators can return a new class. The old experimentalDecorators parameter decorators and reflect-metadata style are a separate, legacy system β for new code, prefer the standard form.
Example
When to use it
- A class decorator logs when any instance of the decorated class is created in a dependency injection container.
- A method decorator wraps async methods to automatically retry on failure without changing the method's business logic.
- An accessor decorator validates that a setter value is within a valid range before assigning it to the property.
More examples
Class decorator (TS 5)
A class decorator wraps the constructor to log whenever a new instance is created, using the TS 5 decorator context.
function Logged<T extends { new (...args: any[]): object }>(Base: T, ctx: ClassDecoratorContext) {
return class extends Base {
constructor(...args: any[]) {
super(...args);
console.log(`Created ${ctx.name}`);
}
};
}
@Logged
class Service {
greet() { return 'hello'; }
}
new Service(); // logs: 'Created Service'Method decorator
A method decorator uses addInitializer to auto-bind the method so it keeps the correct this when destructured.
function Bound(_: unknown, ctx: ClassMethodDecoratorContext) {
ctx.addInitializer(function (this: any) {
this[ctx.name] = this[ctx.name].bind(this);
});
}
class Timer {
private count = 0;
@Bound
tick(): void { this.count++; console.log(this.count); }
}
const { tick } = new Timer();
tick(); // 'this' is correctly boundAccessor decorator
An accessor decorator intercepts the setter to clamp the assigned value between min and max without modifying the class body.
function Clamp(min: number, max: number) {
return function(_: unknown, ctx: ClassAccessorDecoratorContext) {
return {
set(this: any, value: number) {
ctx.access.set(this, Math.min(Math.max(value, min), max));
},
};
};
}
class Slider {
@Clamp(0, 100)
accessor value = 50;
}
const s = new Slider();
s.value = 150;
console.log(s.value); // 100 (clamped)
Discussion