Decorators in TypeScript — annotating and modifying classes declaratively
A decorator is a function that runs when a class (or its members) is DEFINED, not when it's instantiated — which is what makes it possible to wrap, log, or register a class declaratively, right at the point it's declared, instead of writing that logic everywhere the class gets used.
3 min read
The problem: cross-cutting behavior repeated at every use site
class UserService {
getUser(id: string) {
console.log(`Calling getUser with ${id}`); // logging repeated in EVERY method, by hand
const start = performance.now();
const result = this.fetchUser(id);
console.log(`getUser took ${performance.now() - start}ms`);
return result;
}
// every other method needing this same logging has to repeat this boilerplate manually
}Adding logging, timing, or validation around every method the same way means either repeating that logic manually inside every method (real, tedious duplication) or reaching for a different mechanism entirely. This is conceptually the same recurring problem this platform's OOP domain's Decorator pattern lesson addressed with plain object wrapping — TypeScript's decorator syntax is a language-level feature aimed at exactly this kind of cross-cutting concern, applied declaratively.
The fix: a decorator function, applied with @ syntax at the declaration site
function logged(target: Function, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
return function (this: any, ...args: any[]) {
console.log(`Calling ${methodName} with`, args);
const result = target.apply(this, args);
console.log(`${methodName} returned`, result);
return result;
};
}
class UserService {
@logged
getUser(id: string) {
return this.fetchUser(id);
}
}@logged is applied once, at the method's declaration, and TypeScript runs logged as part of setting up the class itself — the decorator receives the original method (target) and returns a replacement function that wraps it, adding logging before and after the real call. Every future call to getUser automatically goes through this wrapper, without a single line of logging code appearing inside getUser's own body.
Decorators run at class DEFINITION time, not at instantiation
function announce(target: Function, context: ClassDecoratorContext) {
console.log(`Defining class: ${context.name}`); // runs ONCE, when the class ITSELF is declared
}
@announce
class Widget {}
// "Defining class: Widget" logs IMMEDIATELY, even before `new Widget()` is ever called anywhereA common early confusion: a decorator's own code runs when the decorated class or method is defined — evaluated once, as the class declaration itself executes — not each time an instance is created with new. This is exactly what makes decorators well-suited for one-time setup concerns (registering a class with a framework, validating that a class meets some contract) as distinct from per-instance behavior, which still has to live in the constructor or methods themselves.
A real, practical decorator: validating a class's shape once, at definition time
function requiresId<T extends { new(...args: any[]): { id: string } }>(constructor: T) {
return class extends constructor {
constructor(...args: any[]) {
super(...args);
if (!this.id) throw new Error("Instances must have an id");
}
};
}
@requiresId
class Product {
constructor(public id: string, public name: string) {}
}This decorator wraps the class's constructor itself, adding a real runtime check that runs on every instantiation — but the decoration (attaching this check to Product) happens once, declaratively, at the class definition, rather than needing to be manually added inside every constructor that has this same requirement. This pattern — a decorator that returns a modified version of what it decorates — is the general shape behind most real, practical uses of class decorators: validation, registration, and dependency-injection frameworks (Angular's @Component, NestJS's @Injectable) all build on exactly this mechanism.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. When does a class or method decorator's own code actually run?
2. In a method decorator like `@logged`, what does the decorator function actually return?
3. What real-world frameworks build on the same class-decorator mechanism covered in this lesson?