🏠 Home / Hub

🔷 TypeScript Lesson 07 — Decorators

← Back to TS Menu  |  🏠 Hub

1. Decorator Setup

// tsconfig.json — enable decorators
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,  // needed for reflect-metadata
    "target": "ES2020"
  }
}

// Decorator = function that receives and can modify class/method/property
// Syntax: @decoratorName (above class/method/property)

// Decorator types:
// @ClassDecorator    — applied to class
// @MethodDecorator   — applied to method
// @PropertyDecorator — applied to property
// @ParameterDecorator— applied to parameter

2. Class Decorator

// Class decorator — receives constructor
function Singleton<T extends { new(...args: any[]): {} }>(constructor: T) {
  let instance: T | null = null;

  return class extends constructor {
    constructor(...args: any[]) {
      if (instance) return instance as any;
      super(...args);
      instance = this as any;
    }
  };
}

function Sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

function Log(constructor: Function) {
  console.log(`Class created: ${constructor.name}`);
}

// Decorator factory — returns decorator (can pass args)
function Route(path: string, method: string = 'GET') {
  return function(constructor: Function) {
    Reflect.defineMetadata('route', path, constructor);
    Reflect.defineMetadata('method', method, constructor);
  };
}

@Singleton
@Sealed
@Log
class DatabaseService {
  constructor(private host: string) {}
  connect() { return `Connected to ${this.host}`; }
}

const db1 = new DatabaseService('localhost');
const db2 = new DatabaseService('remote');
console.log(db1 === db2);  // true — Singleton!

3. Method Decorator

// Method decorator receives: target, propertyKey, descriptor
function Log(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args: any[]) {
    console.log(`▶ ${key}(${JSON.stringify(args)})`);
    const result = original.apply(this, args);
    console.log(`◀ ${key} returned: ${JSON.stringify(result)}`);
    return result;
  };
  return descriptor;
}

function Memoize(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  const cache = new Map<string, any>();

  descriptor.value = function(...args: any[]) {
    const cacheKey = JSON.stringify(args);
    if (cache.has(cacheKey)) {
      console.log(`Cache hit: ${key}(${cacheKey})`);
      return cache.get(cacheKey);
    }
    const result = original.apply(this, args);
    cache.set(cacheKey, result);
    return result;
  };
  return descriptor;
}

function Throttle(ms: number) {
  return function(target: any, key: string, descriptor: PropertyDescriptor) {
    let lastCall = 0;
    const original = descriptor.value;
    descriptor.value = function(...args: any[]) {
      const now = Date.now();
      if (now - lastCall >= ms) {
        lastCall = now;
        return original.apply(this, args);
      }
    };
    return descriptor;
  };
}

class MathService {
  @Log
  add(a: number, b: number): number { return a + b; }

  @Memoize
  expensiveCalc(n: number): number {
    // simulate slow calculation
    return n * n * n;
  }

  @Throttle(1000)
  handleClick(): void {
    console.log("Button clicked!");
  }
}

const svc = new MathService();
svc.add(2, 3);          // logs call + result
svc.expensiveCalc(10);  // calculates
svc.expensiveCalc(10);  // from cache!

4. Property Decorator

function Required(target: any, key: string) {
  let value: any;

  Object.defineProperty(target, key, {
    get() { return value; },
    set(newVal) {
      if (newVal === null || newVal === undefined || newVal === '') {
        throw new Error(`Property '${key}' is required`);
      }
      value = newVal;
    },
    enumerable:   true,
    configurable: true,
  });
}

function MinLength(min: number) {
  return function(target: any, key: string) {
    let value: string;
    Object.defineProperty(target, key, {
      get() { return value; },
      set(v: string) {
        if (v.length < min) throw new Error(`${key} must be at least ${min} chars`);
        value = v;
      },
      enumerable: true, configurable: true,
    });
  };
}

class User {
  @Required
  name!: string;

  @Required
  @MinLength(5)
  password!: string;

  email?: string;
}

const user = new User();
user.name     = 'Ko Ko';     // OK
user.password = 'secret123'; // OK (9 chars)
// user.password = '123';    // throws: password must be at least 5 chars
// user.name = '';           // throws: name is required

5. Real-World: Mini DI Container

// Service registry using decorators
const registry = new Map<string, any>();

function Injectable(name: string) {
  return function(constructor: new (...args: any[]) => any) {
    registry.set(name, constructor);
  };
}

function Inject(name: string) {
  return function(target: any, key: string) {
    Object.defineProperty(target, key, {
      get() {
        const ServiceClass = registry.get(name);
        if (!ServiceClass) throw new Error(`Service '${name}' not registered`);
        return new ServiceClass();
      },
      enumerable: true, configurable: true,
    });
  };
}

@Injectable('logger')
class Logger {
  log(msg: string) { console.log(`[LOG] ${msg}`); }
}

@Injectable('userService')
class UserService {
  @Inject('logger') private logger!: Logger;

  createUser(name: string) {
    this.logger.log(`Creating user: ${name}`);
    return { id: Date.now(), name };
  }
}

// Usage
const svc = registry.get('userService');
if (svc) {
  const service = new svc();
  service.createUser('Ko Ko');  // [LOG] Creating user: Ko Ko
}
💡 NestJS uses decorators heavily: @Module, @Controller, @Get, @Injectable, @Body, @Param

← TS 06  |  Next: TS 08 → Advanced Types →

📌 Study Checklist