A decorator is a function that wraps or annotates a class or one of its members, adding behaviour such as logging, validation, caching or dependency registration without touching the member's own code. TypeScript 5.0 implements the standard TC39 decorator proposal, which needs no compiler flag. This lesson explains how the standard decorators work for each member kind, how to write decorator factories, and how they differ from the older experimentalDecorators mode still used by Angular and NestJS.
Every standard decorator receives two arguments: the value being decorated (the class, method, getter, setter, field initialiser slot or accessor) and a context object describing where it was applied. It may return a replacement value or nothing:
function decorator(value: unknown, context: DecoratorContext) {
context.kind; // "class" | "method" | "getter" | "setter" | "field" | "accessor"
context.name; // the member name (string | symbol)
context.static; // true for static members
context.private; // true for #private members
context.addInitializer(function () { /* runs when the instance (or class) is created */ });
}Decorators are applied with @name directly above a class or member. Several decorators on one target run bottom-up: the one closest to the member is applied first.
A method decorator receives the original function and returns a wrapper (or undefined to leave it unchanged). The sample at the top shows the pattern; here is a memoising variant:
function memoize<This, Arg, Ret>(
target: (this: This, arg: Arg) => Ret,
_context: ClassMethodDecoratorContext<This, (this: This, arg: Arg) => Ret>
) {
const cache = new Map<Arg, Ret>();
return function (this: This, arg: Arg): Ret {
if (!cache.has(arg)) cache.set(arg, target.call(this, arg));
return cache.get(arg)!;
};
}
class Math2 {
@memoize
fib(n: number): number { return n < 2 ? n : this.fib(n - 1) + this.fib(n - 2); }
}Getter and setter decorators work the same way with ClassGetterDecoratorContext and ClassSetterDecoratorContext.
A class decorator receives the constructor and may return a subclass to extend it:
function registered<T extends new (...args: any[]) => object>(target: T, context: ClassDecoratorContext<T>) {
registry.set(String(context.name), target);
return class extends target {
createdAt = new Date();
};
}
const registry = new Map<string, unknown>();
@registered
class Service {}A field decorator receives undefined as its value (fields have no value yet) and may return an initialiser function that transforms the initial value:
function trimmed(_value: undefined, context: ClassFieldDecoratorContext<unknown, string>) {
return (initial: string) => initial.trim();
}
class Form {
@trimmed name = " Ada ";
}
new Form().name; // "Ada"To pass options, write a function that returns a decorator:
function throttle(ms: number) {
return function <This, Args extends unknown[]>(
target: (this: This, ...args: Args) => void,
_context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => void>
) {
let last = 0;
return function (this: This, ...args: Args) {
const now = Date.now();
if (now - last >= ms) { last = now; target.call(this, ...args); }
};
};
}
class Scroller {
@throttle(100)
onScroll(y: number) { console.log(y); }
}| Aspect | Standard (TS 5.0+) | Legacy experimentalDecorators |
|---|---|---|
| Flag required | None | "experimentalDecorators": true |
| Signature | (value, context) | (target, key, descriptor) |
| Parameter decorators | Not supported | Supported |
| Type metadata | context.metadata (TS 5.2, needs Symbol.metadata) | emitDecoratorMetadata + reflect-metadata |
| Used by | New libraries, plain applications | Angular, NestJS, TypeORM, class-validator |
The two modes have incompatible signatures, and a project uses one or the other. When a framework's documentation tells you to enable experimentalDecorators, follow it; otherwise prefer the standard form, which will keep working as runtimes ship native decorators.
this; use a function expression with a this parameter.context.addInitializer for per-instance setup.(target, key, descriptor) decorators from an old tutorial into a standard-decorator project.What two arguments does a standard TypeScript 5 decorator receive?
(value, context) and need no compiler flag.context.kind, context.name and context.addInitializer describe and hook into the decorated member.experimentalDecorators has a different signature and is still required by Angular, NestJS and TypeORM.Next lesson: Working with the DOM — type elements, events and query results in browser code.