Decorators (Experimental)

Difficulty: Advanced

Question

What are decorators in TypeScript? Explain the different types and their use cases.

Answer

Decorators are special functions that can modify classes, methods, properties, and parameters at declaration time. They use the @expression syntax placed before the target.

TypeScript has two decorator systems: 1. Legacy/Experimental decorators (experimentalDecorators: true) - used by Angular, NestJS, TypeORM 2. TC39 Stage 3 decorators (TypeScript 5.0+) - the standard going forward

Decorator types: - Class decorators: modify or replace the class constructor - Method decorators: wrap or modify method behavior - Property decorators: add metadata to properties - Parameter decorators: add metadata to method parameters

Decorators enable declarative patterns: logging, validation, dependency injection, ORM mappings, and API route definitions.

Code examples

Class and Method Decorators

// Method decorator: logging wrapper
function Log(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
): PropertyDescriptor {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey} with args:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`${propertyKey} returned:`, result);
    return result;
  };
  return descriptor;
}

// Class decorator: sealed (prevent extensions)
function Sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.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);
// Calling add with args: [2, 3]
// add returned: 5

Method decorators receive the target, method name, and property descriptor. They can wrap the original method to add cross-cutting behavior like logging, timing, or caching.

Real-World: NestJS-Style Decorators

// Simulating NestJS controller decorators
function Controller(basePath: string) {
  return function (constructor: Function) {
    Reflect.defineMetadata('basePath', basePath, constructor);
  };
}

function Get(path: string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    Reflect.defineMetadata('method', 'GET', target, propertyKey);
    Reflect.defineMetadata('path', path, target, propertyKey);
  };
}

function Validate(schema: object) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const original = descriptor.value;
    descriptor.value = function (req: any, res: any) {
      // validate req.body against schema
      return original.call(this, req, res);
    };
  };
}

@Controller('/users')
class UserController {
  @Get('/')
  listUsers(req: any, res: any) {
    return res.json([]);
  }

  @Get('/:id')
  getUser(req: any, res: any) {
    return res.json({ id: req.params.id });
  }
}

Frameworks like NestJS and Angular use decorators heavily for declarative routing, dependency injection, and metadata. The decorator stores metadata that the framework reads at startup.

TC39 Stage 3 Decorators (TS 5.0+)

// New standard decorators (no experimentalDecorators flag needed)
function logged<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
  const methodName = String(context.name);
  return function (this: This, ...args: Args): Return {
    console.log(`Entering ${methodName}`);
    const result = target.call(this, ...args);
    console.log(`Exiting ${methodName}`);
    return result;
  };
}

function bound<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
  context.addInitializer(function (this: This) {
    const self = this as any;
    self[context.name] = self[context.name].bind(self);
  });
}

class TaskRunner {
  name = 'Runner';

  @logged
  @bound
  run(task: string): string {
    return `${this.name} completed ${task}`;
  }
}

const runner = new TaskRunner();
const runFn = runner.run; // safely unbound due to @bound
runFn('deploy'); // 'Runner completed deploy'

TC39 Stage 3 decorators are type-safe by design, using context objects instead of reflect-metadata. They are the future standard and work without experimentalDecorators.

Key points

Concepts covered

Decorators, Class Decorators, Method Decorators, Property Decorators, Metadata, TC39 Stage 3