TypeScript Decorators

Decorators attach reusable behavior to classes, methods, and properties by wrapping them with a special function at definition time.

Enabling Experimental Decorators

Decorators are still an experimental TypeScript feature (distinct from the newer TC39 stage-3 decorators shipped by default in modern TypeScript). To use the classic syntax shown here, enable `"experimentalDecorators": true` in `tsconfig.json`, and add `"emitDecoratorMetadata": true` if you rely on frameworks like Angular or NestJS that read parameter types at runtime.

Class Decorators

A class decorator is a function that receives the constructor and runs once, right when the class is defined - not when an instance is created. It can log, register, or even replace the constructor entirely by returning a new one.

A Simple Class Decorator

function Logger(constructor: Function): void {
  console.log(`Creating instance of ${constructor.name}`);
}

@Logger
class UserService {
  constructor(public name: string) {}
}

new UserService("Ada");

Method Decorators

A method decorator receives the class prototype, the method name, and a `PropertyDescriptor`. Replacing `descriptor.value` lets you wrap the original method - a common use is measuring execution time or adding logging without touching the method body itself.

Timing a Method Call

function LogExecutionTime(
  target: unknown,
  propertyKey: string,
  descriptor: PropertyDescriptor
): PropertyDescriptor {
  const original = descriptor.value;
  descriptor.value = function (this: unknown, ...args: unknown[]) {
    const start = Date.now();
    const result = original.apply(this, args);
    console.log(`${propertyKey} took ${Date.now() - start}ms`);
    return result;
  };
  return descriptor;
}

class Report {
  @LogExecutionTime
  generate(rows: number): string {
    let total = 0;
    for (let i = 0; i < rows; i++) total += i;
    return `Processed ${rows} rows, sum ${total}`;
  }
}

new Report().generate(1000);

Decorator Factories

A decorator factory is a function that returns a decorator, which lets you pass configuration into it. This is how framework decorators like `@Component("selector")` accept arguments while still following the plain decorator signature underneath.

A Decorator Factory

function Component(selector: string) {
  return function (constructor: Function): void {
    (constructor as unknown as { selector: string }).selector = selector;
  };
}

@Component("app-user-card")
class UserCard {
  constructor(public title: string) {}
}

console.log((UserCard as unknown as { selector: string }).selector);
  • Class decorators - applied to the constructor
  • Method decorators - applied to a method's property descriptor
  • Accessor decorators - applied to a get/set pair
  • Property decorators - applied to a class field
  • Parameter decorators - applied to a single function argument
Decorator typeApplied toParameters received
ClassClass declarationconstructor
MethodMethod definitiontarget, propertyKey, descriptor
Accessorget/set pairtarget, propertyKey, descriptor
PropertyClass fieldtarget, propertyKey
ParameterFunction argumenttarget, propertyKey, parameterIndex
Note: Stack multiple decorators on the same declaration and they run bottom-up for evaluation but top-down for the actual call - useful to remember when order matters, such as combining `@LogExecutionTime` with an `@Authorize` decorator.
Note: These experimental decorators use a different runtime shape than the TC39 stage-3 decorators TypeScript now supports without the experimental flag - mixing tutorials or library code written for one against a project configured for the other will cause confusing type errors.

Exercise: TypeScript Decorators

At what point does a class decorator function actually execute?