Skip to content
intermediate Phase 6 · TypeScript Design Patterns

Decorator & Adapter Patterns

Use structural patterns for extending functionality and interface adaptation.

1h
0 problems
Topic Progress 0%

Decorator Fundamentals

Decorator Fundamentals

Class Decorator

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

@Sealed
class MyClass {
  property = 'value';
  method() { return 42; }
}

// Cannot add new properties or methods
// (MyClass.prototype as any).newMethod = () => {}; // Error in runtime

Method Decorator

function Log(
  target: any,
  propertyKey: string,
  descriptor: 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;
  };
}

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

new Calculator().add(2, 3);
// Calling add with args: [2, 3]
// add returned: 5

Decorator Factories

Decorator Factories

Configurable Decorators

function Throttle(delay: number) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    let lastCall = 0;
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      const now = Date.now();
      if (now - lastCall >= delay) {
        lastCall = now;
        return originalMethod.apply(this, args);
      }
    };
  };
}

class SearchService {
  @Throttle(300)
  search(query: string): void {
    console.log(`Searching: ${query}`);
  }
}

Property Decorator

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

  const getter = () => value;
  const setter = (newVal: any) => {
    if (newVal === undefined || newVal === null) {
      throw new Error(`${propertyKey} is required`);
    }
    value = newVal;
  };

  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true
  });
}

class User {
  @Required
  name!: string;

  @Required
  email!: string;
}

const user = new User();
user.name = 'Alice'; // OK
// user.name = null; // Error: name is required

Practical Decorators

Practical Decorators

Memoize Decorator

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

  descriptor.value = function (...args: any[]) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = originalMethod.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

class MathService {
  @Memoize
  fibonacci(n: number): number {
    if (n <= 1) return n;
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}

Validate Decorator

function Validate(schema: ZodSchema) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      const result = schema.safeParse(args[0]);
      if (!result.success) {
        throw new Error(`Validation failed: ${result.error.message}`);
      }
      return originalMethod.apply(this, args);
    };
  };
}

Retry Decorator

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

    descriptor.value = async function (...args: any[]) {
      for (let i = 0; i <= maxRetries; i++) {
        try {
          return await originalMethod.apply(this, args);
        } catch (error) {
          if (i === maxRetries) throw error;
          await new Promise(r => setTimeout(r, delay * (i + 1)));
        }
      }
    };
  };
}

TC39 Decorators

TC39 Decorators

New Standard vs Legacy

// Legacy decorators (experimentalDecorators: true in tsconfig)
function Log(target: any, key: string, descriptor: PropertyDescriptor) { /* ... */ }

// TC39 decorators (new standard)
function Log<T extends (...args: any[]) => any>(
  originalMethod: T,
  context: ClassMethodDecoratorContext
) {
  return function (this: any, ...args: Parameters<T>): ReturnType<T> {
    console.log(`Calling ${String(context.name)}`);
    return originalMethod.call(this, ...args);
  };
}

class UserService {
  @Log
  getUser(id: string) {
    return { id, name: 'Alice' };
  }
}

TC39 Access Decorators

function Override(original: any, context: ClassAccessorDecoratorContext) {
  return {
    get(this: any) {
      return original.get.call(this);
    },
    set(this: any, value: any) {
      // Custom logic
      original.set.call(this, value);
    }
  };
}

class Settings {
  @Override
  accessor theme: string = 'light';
}

Configuration

// tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,  // legacy mode
    "emitDecoratorMetadata": true     // reflect metadata
  }
}

Best Practices

  • Keep decorators focused on single responsibility
  • Document decorator behavior clearly
  • Use decorator factories for configuration
  • Prefer TC39 decorators for new projects
  • Test decorator behavior independently