Skip to editor content
learningtypescript.orglesson 11 of 25

Decorators

Decorators are a way to add annotations and modify classes and their members. They're widely used in frameworks like Angular, NestJS, and TypeORM.

What Are Decorators?

A decorator is a function that receives information about the thing being decorated and can modify its behavior.

// Class decorator — a function that takes a class
function Logger(constructor: Function) {
  console.log(`Class created: ${constructor.name}`);
}

// Simulating decorator behavior (since playground may not support @ syntax)
class UserService {
  getUser(id: number) {
    return { id, name: "Alice" };
  }
}

// Apply decorator manually
Logger(UserService);

// Decorator factory — returns a decorator
function Component(options: { name: string; version: string }) {
  return function(constructor: Function) {
    console.log(`Registered component: ${options.name} v${options.version}`);
    (constructor as any).componentName = options.name;
    (constructor as any).version = options.version;
  };
}

class AppHeader {
  render() { return "<header>App</header>"; }
}

Component({ name: "AppHeader", version: "1.0" })(AppHeader);
console.log(`Component: ${(AppHeader as any).componentName}`);

Method Decorators

Method decorators wrap or modify class methods.

// Logging decorator
function log(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args: any[]) {
    console.log(`Calling ${key}(${args.join(", ")})`);
    const result = original.apply(this, args);
    console.log(`  -> returned: ${JSON.stringify(result)}`);
    return result;
  };
}

// Timing decorator
function measure(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args: any[]) {
    const start = performance.now();
    const result = original.apply(this, args);
    const end = performance.now();
    console.log(`${key} took ${(end - start).toFixed(3)}ms`);
    return result;
  };
}

class Calculator {
  add(a: number, b: number): number { return a + b; }
  multiply(a: number, b: number): number { return a * b; }
  fibonacci(n: number): number {
    if (n <= 1) return n;
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}

// Apply decorators manually
const calc = new Calculator();
const addDesc = Object.getOwnPropertyDescriptor(Calculator.prototype, "add")!;
log(Calculator.prototype, "add", addDesc);
Object.defineProperty(Calculator.prototype, "add", addDesc);

const fibDesc = Object.getOwnPropertyDescriptor(Calculator.prototype, "fibonacci")!;
measure(Calculator.prototype, "fibonacci", fibDesc);
Object.defineProperty(Calculator.prototype, "fibonacci", fibDesc);

calc.add(3, 4);
calc.fibonacci(20);

Validation Decorators

Decorators can enforce runtime validation.

// Simple validation system
const validations = new Map<string, Map<string, Function[]>>();

function validate(validator: (value: any) => boolean, message: string) {
  return function(target: any, propertyKey: string) {
    const className = target.constructor.name;
    if (!validations.has(className)) {
      validations.set(className, new Map());
    }
    const classValidations = validations.get(className)!;
    if (!classValidations.has(propertyKey)) {
      classValidations.set(propertyKey, []);
    }
    classValidations.get(propertyKey)!.push((value: any) => {
      if (!validator(value)) throw new Error(`${propertyKey}: ${message}`);
    });
  };
}

class UserForm {
  name: string = "";
  email: string = "";
  age: number = 0;

  constructor(data: { name: string; email: string; age: number }) {
    this.name = data.name;
    this.email = data.email;
    this.age = data.age;
  }
}

// Register validations
validate((v: any) => typeof v === "string" && v.length >= 2, "must be at least 2 chars")(
  UserForm.prototype, "name"
);
validate((v: any) => typeof v === "string" && v.includes("@"), "must be valid email")(
  UserForm.prototype, "email"
);
validate((v: any) => typeof v === "number" && v >= 0 && v <= 150, "must be 0-150")(
  UserForm.prototype, "age"
);

// Validate an instance
function validateInstance(instance: any): string[] {
  const className = instance.constructor.name;
  const classValidations = validations.get(className);
  if (!classValidations) return [];

  const errors: string[] = [];
  classValidations.forEach((validators, field) => {
    validators.forEach(validator => {
      try { validator(instance[field]); }
      catch (e: any) { errors.push(e.message); }
    });
  });
  return errors;
}

const validUser = new UserForm({ name: "Alice", email: "alice@example.com", age: 30 });
console.log("Valid user errors:", validateInstance(validUser));

const invalidUser = new UserForm({ name: "A", email: "bad", age: -5 });
console.log("Invalid user errors:", validateInstance(invalidUser));

Decorator Factories

Factories let you configure decorator behavior with parameters.

// Retry decorator factory
function retry(maxAttempts: number) {
  return function(target: any, key: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value;
    descriptor.value = function(...args: any[]) {
      let lastError: Error | undefined;
      for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
          return original.apply(this, args);
        } catch (error: any) {
          lastError = error;
          console.log(`  Attempt ${attempt}/${maxAttempts} failed: ${error.message}`);
        }
      }
      throw lastError;
    };
  };
}

class ApiClient {
  private callCount = 0;

  fetchData(): string {
    this.callCount++;
    if (this.callCount < 3) {
      throw new Error("Network timeout");
    }
    return "Data received!";
  }
}

const client = new ApiClient();
const fetchDesc = Object.getOwnPropertyDescriptor(ApiClient.prototype, "fetchData")!;
retry(5)(ApiClient.prototype, "fetchData", fetchDesc);
Object.defineProperty(ApiClient.prototype, "fetchData", fetchDesc);

try {
  const result = client.fetchData();
  console.log(`Result: ${result}`);
} catch (e: any) {
  console.log(`Failed after all retries: ${e.message}`);
}

Try It Yourself

// Build a simple dependency injection container
class Container {
  private services = new Map<string, any>();

  register(name: string, factory: () => any): void {
    this.services.set(name, factory);
  }

  resolve<T>(name: string): T {
    const factory = this.services.get(name);
    if (!factory) throw new Error(`Service '${name}' not registered`);
    return factory();
  }
}

// Services
class Database {
  query(sql: string): string[] {
    return [`Result for: ${sql}`];
  }
}

class UserRepository {
  constructor(private db: Database) {}

  findAll(): string[] {
    return this.db.query("SELECT * FROM users");
  }
}

class UserController {
  constructor(private repo: UserRepository) {}

  listUsers(): void {
    const users = this.repo.findAll();
    console.log("Users:", users);
  }
}

// Wire up dependencies
const container = new Container();
container.register("database", () => new Database());
container.register("userRepo", () => new UserRepository(container.resolve("database")));
container.register("userController", () => new UserController(container.resolve("userRepo")));

const controller = container.resolve<UserController>("userController");
controller.listUsers();

Key Takeaways

  • Decorators are functions that modify classes, methods, or properties
  • Decorator factories return decorators and accept configuration
  • Method decorators receive the target, property key, and descriptor
  • Decorators are widely used in frameworks like Angular and NestJS
  • They enable cross-cutting concerns like logging, validation, and caching
  • Combine multiple decorators — they compose from bottom to top

Pro Tip: Use decorators for cross-cutting concerns that span many classes: logging, caching, authentication checks, validation. Keep business logic in the methods themselves. If a decorator does too much, split it into multiple focused decorators that compose well.

Next Steps

You've built classes with decorators — but how do you verify they work correctly? Next, you'll learn how to write type-safe tests with assertion patterns, typed mocks, and compile-time type testing.

Next lesson

Testing

Write type-safe tests in TypeScript with assertion patterns, mocking, and test organization for reliable, maintainable test suites.

25 min