⚡IntermediateClasses & Decorators
Decorators
Add metadata and behavior to classes, methods, and properties using TypeScript decorators - the foundation of frameworks like Angular and NestJS.
Overview
**Decorators** are a syntax for annotating and modifying class declarations, methods, accessors, properties, and parameters. They use the `@expression` syntax placed immediately above the declaration they annotate.
TypeScript supports two decorator specifications:
- **Experimental decorators** (enabled with `"experimentalDecorators": true` in tsconfig) - the older, widely-used stage-2 proposal used by Angular and NestJS
- **TC39 Stage 3 decorators** - the new standard supported in TypeScript 5.0+ without any tsconfig flag
This topic covers the **stage-3 / TypeScript 5** decorator model, which is the forward-compatible approach. Decorators in this model receive a well-typed `context` object describing what they are decorating, making them type-safe and composable.
Decorators are evaluated at class definition time (not instantiation time), making them ideal for registering metadata, wrapping methods with logging or validation, and implementing aspect-oriented patterns.
Key Points
- •Decorator syntax: `@myDecorator` placed above a class, method, accessor, field, or parameter
- •TypeScript 5+ decorators (TC39 stage 3) require no tsconfig flag; experimental decorators need `"experimentalDecorators": true`
- •Class decorator: receives the class constructor and optional context; can return a new class
- •Method decorator: receives the method function and context; can replace the method with a wrapper
- •Field decorator: receives `undefined` and context; returns an initializer function
- •Decorators are applied bottom-up when stacked: `@A @B` means B runs first, then A
- •Used in Angular (Component, Injectable), NestJS (Controller, Get, Injectable), and class-validator
Examples
Class and method decorators (TypeScript 5 style)
// Method decorator - logs calls
function log(target: Function, context: ClassMethodDecoratorContext): Function {
const methodName = String(context.name);
return function (this: unknown, ...args: unknown[]) {
console.log(`[${methodName}] called with:`, args);
const result = (target as Function).apply(this, args);
console.log(`[${methodName}] returned:`, result);
return result;
};
}
// Class decorator - seals the class (no new properties after creation)
function sealed(target: Function, _context: ClassDecoratorContext): void {
Object.seal(target);
Object.seal(target.prototype);
}
@sealed
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
@log
multiply(a: number, b: number): number {
return a * b;
}
}
const calc = new Calculator();
calc.add(2, 3);
// [add] called with: [2, 3]
// [add] returned: 5Decorator factories - decorators that take arguments
// A decorator factory returns a decorator
function validate(min: number, max: number) {
return function (
target: Function,
context: ClassMethodDecoratorContext
): Function {
return function (this: unknown, value: number) {
if (value < min || value > max) {
throw new RangeError(
`${String(context.name)}: ${value} is not in range [${min}, ${max}]`
);
}
return (target as Function).call(this, value);
};
};
}
function readonly(_: undefined, context: ClassFieldDecoratorContext) {
return function (this: unknown, initialValue: unknown) {
Object.defineProperty(this, context.name, {
value: initialValue,
writable: false,
configurable: false,
});
return initialValue;
};
}
class Temperature {
@readonly
unit: string = 'Celsius';
@validate(-273.15, 1e6)
setTemperature(value: number): number {
console.log(`Temperature set to ${value}°${this.unit}`);
return value;
}
}
const t = new Temperature();
t.setTemperature(100); // Temperature set to 100°Celsius
try {
t.setTemperature(-300); // RangeError
} catch (e) {
console.error((e as Error).message);
}Decorator composition and metadata patterns
// Track which methods are "deprecated"
const deprecatedMethods = new Set<string>();
function deprecated(_: Function, context: ClassMethodDecoratorContext): Function {
deprecatedMethods.add(String(context.name));
return function (this: unknown, ...args: unknown[]) {
console.warn(`[DEPRECATED] ${String(context.name)} will be removed in the next version`);
return (_ as Function).apply(this, args);
};
}
// Memoize - cache method results
function memoize(target: Function, context: ClassMethodDecoratorContext): Function {
const cache = new Map<string, unknown>();
return function (this: unknown, ...args: unknown[]) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log(`[cache hit] ${String(context.name)}(${key})`);
return cache.get(key);
}
const result = (target as Function).apply(this, args);
cache.set(key, result);
return result;
};
}
class MathService {
@deprecated
oldAdd(a: number, b: number): number { return a + b; }
@memoize
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
const svc = new MathService();
svc.oldAdd(1, 2); // warns: [DEPRECATED]
svc.fibonacci(10); // computes
svc.fibonacci(10); // [cache hit]Common Mistakes
- Mixing experimental decorators (`experimentalDecorators: true`) with TypeScript 5 stage-3 decorators - they are incompatible; pick one and be consistent
- Forgetting that decorators run at class definition time, not each time an instance is created - storing per-instance state in a decorator closure affects all instances
- Stacking decorators `@A @B` and expecting A to run first - decorators apply bottom-up: B runs before A
Note
Decorator factories (functions that return decorators) are the composable form: `@validate(0, 100)` is cleaner than `@validateBetween0And100`. Always write decorators as factories when you have any parameters - even if the first version takes no args, you'll thank yourself when requirements change.
Exercises
Write TypeScript code to solve each exercise. Click Run to compile and execute it right here, then mark as done.
Loading editor…
Completed: 0 / 3 exercises
Knowledge Check
Question 1 of 4