DEV Community

Atlas Whoff
Atlas Whoff

Posted on

TypeScript Decorators: Metaprogramming Patterns for Clean Code

TypeScript Decorators: Metaprogramming Patterns for Clean Code

Decorators let you attach behavior to classes, methods, and properties without modifying their internals. They're used everywhere in NestJS, TypeORM, and Angular — and for good reason.

What Decorators Are

A decorator is a function that wraps another function, class, or property. The @ syntax is sugar for passing the target as an argument:

@Log  // is equivalent to: MyClass = Log(MyClass)
class MyClass {}
Enter fullscreen mode Exit fullscreen mode

Method Decorators

function Retry(attempts: number) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (...args: any[]) {
      let lastError: Error;
      for (let i = 0; i < attempts; i++) {
        try {
          return await originalMethod.apply(this, args);
        } catch (err) {
          lastError = err as Error;
          if (i < attempts - 1) {
            await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
          }
        }
      }
      throw lastError!;
    };

    return descriptor;
  };
}

class PaymentService {
  @Retry(3)  // Automatically retries with exponential backoff
  async chargeCard(amount: number) {
    return await stripeClient.charge(amount);
  }
}
Enter fullscreen mode Exit fullscreen mode

Class Decorators

function Singleton<T extends { new(...args: any[]): {} }>(constructor: T) {
  let instance: InstanceType<T>;

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

@Singleton
class DatabaseConnection {
  private pool = createPool();
  // Guaranteed single instance across the app
}
Enter fullscreen mode Exit fullscreen mode

Property Decorators for Validation

function MinLength(min: number) {
  return function (target: any, propertyKey: string) {
    let value: string;

    Object.defineProperty(target, propertyKey, {
      get: () => value,
      set: (newValue: string) => {
        if (newValue.length < min) {
          throw new Error(`${propertyKey} must be at least ${min} characters`);
        }
        value = newValue;
      },
    });
  };
}

class User {
  @MinLength(8)
  password: string = '';
}
Enter fullscreen mode Exit fullscreen mode

Real-World: Logging Decorator

function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;

  descriptor.value = async function (...args: any[]) {
    const start = Date.now();
    console.log(`[${propertyKey}] called with`, args);

    try {
      const result = await original.apply(this, args);
      console.log(`[${propertyKey}] completed in ${Date.now() - start}ms`);
      return result;
    } catch (err) {
      console.error(`[${propertyKey}] failed after ${Date.now() - start}ms:`, err);
      throw err;
    }
  };
}

class OrderService {
  @Log
  async createOrder(data: CreateOrderDto) { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

Enable Decorators

// tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Decorators — alongside dependency injection, module patterns, and clean architecture — are part of the advanced TypeScript patterns in the Ship Fast Skill Pack skill set.

Top comments (0)