Concept
Version-currency callout, read this first: a large fraction of existing decorator tutorials, Stack Overflow answers, and even some popular libraries' documentation still teach TypeScript's original, pre-standardization decorator design, enabled via the experimentalDecorators compiler flag. As of TypeScript 5.0, that's no longer the default: standard, TC39 decorators (the same decorator proposal now part of JavaScript itself, not a TypeScript-only feature) are what TypeScript expects out of the box, with a meaningfully different function signature. This topic teaches the modern standard first; the legacy behavior gets a compatibility footnote at the end, not equal billing.
The core mechanic: a decorator is a function applied to a class or class member
function logged(target: any, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
return function (this: any, ...args: any[]) {
console.log(`calling ${methodName}`);
return target.call(this, ...args);
};
}
class Greeter {
@logged
greet(name: string) {
return `Hello, ${name}`;
}
}
new Greeter().greet("Ada");
// logs: "calling greet"
// then: "Hello, Ada"Confirmed by compiling this with no special flag and actually running the resulting JavaScript: the @logged decorator wraps greet, and the wrapped version runs in its place, logging before delegating to the original implementation. The decorator function receives exactly two arguments: the thing being decorated (target, here, the original method) and a context object describing it (context.name gives the method's name as a string).
Class decorators: replacing or extending the whole class
function logged<T extends new (...args: any[]) => any>(target: T, context: ClassDecoratorContext) {
console.log(`decorating class: ${context.name}`);
return class extends target {
createdAt = new Date();
};
}
@logged
class Point {
x = 1;
y = 2;
}
const p =
Confirmed by compiling and running: a class decorator receives the class constructor itself as target, and can return an entirely new class (here, a subclass adding a createdAt field) to replace the original, the decoration runs once, at class definition time, logging "decorating class: Point" before any instance is ever created.
Field decorators and context.addInitializer
function initLog(value: undefined, context: ClassFieldDecoratorContext) {
context.addInitializer(function (this: any) {
console.log(`field ${String(context.name)} initialized`);
});
}
class Widget {
@initLog
label = "hello";
}
new Widget(); // logs: "field label initialized"Confirmed by compiling and running: field decorators receive the field's initial value (often undefined at decoration time, since decoration happens before instances exist) and a context object exposing addInitializer, a way to register a callback that runs during actual instance construction, useful for setup logic that needs to happen per-instance rather than once at class-definition time.