Advanced decorator patterns and metadata.

Parameter Decorators with Metadata

import "reflect-metadata";

const requiredMetadataKey = Symbol("required");

function required(
  target: any,
  propertyKey: string | symbol,
  parameterIndex: number
) {
  const existingRequiredParameters: number[] =
    Reflect.getMetadata(requiredMetadataKey, target, propertyKey) || [];
  existingRequiredParameters.push(parameterIndex);
  Reflect.defineMetadata(
    requiredMetadataKey,
    existingRequiredParameters,
    target,
    propertyKey
  );
}

function validate(
  target: any,
  propertyName: string,
  descriptor: PropertyDescriptor
) {
  const method = descriptor.value;
  const requiredParameters: number[] = Reflect.getMetadata(
    requiredMetadataKey,
    target,
    propertyName
  );

  descriptor.value = function (...args: any[]) {
    if (requiredParameters) {
      for (const parameterIndex of requiredParameters) {
        if (parameterIndex >= args.length || args[parameterIndex] == null) {
          throw new Error(`Parameter at index ${parameterIndex} is required`);
        }
      }
    }
    return method.apply(this, args);
  };
}

class UserService {
  @validate
  createUser(@required name: string, age: number) {
    // ...
  }
}

Class Decorator Factory

function Entity(tableName: string) {
  return function <T extends { new (...args: any[]): {} }>(constructor: T) {
    return class extends constructor {
      static tableName = tableName;
      id: number = 0;
    };
  };
}

@Entity("users")
class User {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

console.log((User as any).tableName); // "users"

Method Decorator with Caching

const cache = new Map<string, any>();

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

  descriptor.value = function (...args: any[]) {
    const key = `${propertyKey}_${JSON.stringify(args)}`;

    if (cache.has(key)) {
      return cache.get(key);
    }

    const result = originalMethod.apply(this, args);
    cache.set(key, result);
    return result;
  };

  return descriptor;
}

class Calculator {
  @cacheResult
  expensiveOperation(n: number): number {
    // Expensive computation
    return n * n;
  }
}

Property Decorator with Validation

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

    const getter = () => value;
    const setter = (newVal: number) => {
      if (newVal < min || newVal > max) {
        throw new Error(
          `Value must be between ${min} and ${max}, got ${newVal}`
        );
      }
      value = newVal;
    };

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

class Product {
  @validateRange(0, 100)
  discount: number = 0;
}

Multiple Decorators

function first() {
  console.log("first(): factory evaluated");
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    console.log("first(): called");
  };
}

function second() {
  console.log("second(): factory evaluated");
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    console.log("second(): called");
  };
}

class Example {
  @first()
  @second()
  method() {}
}

// Output:
// first(): factory evaluated
// second(): factory evaluated
// second(): called
// first(): called

Exercises

Exercise 1: Timing Decorator

// Create a decorator that:
// - Measures execution time
// - Logs the duration
// - Works with async methods

Exercise 2: Retry Decorator

// Create a decorator that:
// - Retries a method on failure
// - Takes max retries as parameter
// - Handles errors appropriately

Exercise 3: Logging Decorator

// Create a decorator that:
// - Logs method calls with parameters
// - Logs return values
// - Can be enabled/disabled

Exercise 4: Authorization Decorator

// Create a decorator that:
// - Checks user permissions
// - Throws error if unauthorized
// - Works with role-based access

Exercise 5: Transaction Decorator

// Create a decorator that:
// - Wraps method in a transaction
// - Rolls back on error
// - Commits on success

Best Practices

  1. Use decorator factories - For configurable decorators
  2. Store metadata properly - Use reflect-metadata
  3. Handle errors - Don't let decorators break silently
  4. Document behavior - Explain what decorators do
  5. Test thoroughly - Ensure decorators work correctly

Common Mistakes

  1. Forgetting to return descriptor
// ❌ Wrong
function decorator(target: any, key: string, desc: PropertyDescriptor) {
  desc.value = function() { /* ... */ };
  // Missing return
}

// ✅ Correct
function decorator(target: any, key: string, desc: PropertyDescriptor) {
  desc.value = function() { /* ... */ };
  return desc;
}
  1. Not handling async methods
// ❌ Doesn't handle promises
function decorator(target: any, key: string, desc: PropertyDescriptor) {
  const result = desc.value();
  // result might be a Promise
}

// ✅ Handle async
function decorator(target: any, key: string, desc: PropertyDescriptor) {
  const original = desc.value;
  desc.value = async function(...args: any[]) {
    return await original.apply(this, args);
  };
}
  1. Circular dependencies
// ❌ Might cause issues
@decoratorA
class A {
  @decoratorB
  method() {}
}

Next Steps

Learn about Module Augmentation.