Skip to main content
โšก Calmops

Decorators and Metadata in TypeScript

Decorators and Metadata in TypeScript

Decorators enable powerful metaprogramming capabilities. This article covers decorator types, metadata, and practical applications.

Introduction

Decorators provide:

  • Code enhancement
  • Metaprogramming
  • Reflection
  • Cross-cutting concerns
  • Framework capabilities

Understanding decorators helps you:

  • Enhance classes and methods
  • Implement frameworks
  • Add metadata
  • Create reusable patterns
  • Build powerful abstractions

Enabling Decorators

Configuration

// โœ… Good: Enable decorators in tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "target": "ES2020",
    "module": "commonjs"
  }
}

Class Decorators

Basic Class Decorators

// โœ… Good: Basic class decorator
function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class User {
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

// โœ… Good: Class decorator with parameters
function logged(target: Function) {
  const original = target;

  const newConstructor: any = function (...args: any[]) {
    console.log(`Creating instance of ${original.name}`);
    return new original(...args);
  };

  newConstructor.prototype = original.prototype;
  return newConstructor;
}

@logged
class Product {
  constructor(public name: string) {}
}

// โœ… Good: Decorator factory
function decorator(param: string) {
  return function (target: Function) {
    console.log(`Decorating ${target.name} with ${param}`);
  };
}

@decorator('param')
class MyClass {}

Practical Class Decorators

// โœ… Good: Validation decorator
function validate(target: Function) {
  const original = target;

  const newConstructor: any = function (...args: any[]) {
    const instance = new original(...args);

    // Validate instance
    for (const key in instance) {
      if (typeof instance[key] === 'string' && instance[key].length === 0) {
        throw new Error(`${key} cannot be empty`);
      }
    }

    return instance;
  };

  newConstructor.prototype = original.prototype;
  return newConstructor;
}

@validate
class User {
  constructor(public name: string, public email: string) {}
}

// โœ… Good: Singleton decorator
function singleton(target: Function) {
  let instance: any;

  const newConstructor: any = function (...args: any[]) {
    if (!instance) {
      instance = new target(...args);
    }
    return instance;
  };

  newConstructor.prototype = target.prototype;
  return newConstructor;
}

@singleton
class Database {
  constructor(public url: string) {}
}

Method Decorators

Basic Method Decorators

// โœ… Good: Basic 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);
    const result = originalMethod.apply(this, args);
    console.log(`Result:`, result);
    return result;
  };

  return descriptor;
}

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

// โœ… Good: Timing decorator
function timing(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    const start = performance.now();
    const result = originalMethod.apply(this, args);
    const end = performance.now();
    console.log(`${propertyKey} took ${end - start}ms`);
    return result;
  };

  return descriptor;
}

class Service {
  @timing
  slowOperation() {
    // Simulate slow operation
    for (let i = 0; i < 1000000; i++) {}
  }
}

Async Method Decorators

// โœ… Good: Async method decorator
function asyncLog(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;

  descriptor.value = async function (...args: any[]) {
    console.log(`Calling async ${propertyKey}`);
    try {
      const result = await originalMethod.apply(this, args);
      console.log(`${propertyKey} completed`);
      return result;
    } catch (error) {
      console.error(`${propertyKey} failed:`, error);
      throw error;
    }
  };

  return descriptor;
}

class ApiService {
  @asyncLog
  async fetchUser(id: number) {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
  }
}

Property Decorators

Basic Property Decorators

// โœ… Good: Property decorator
function required(target: any, propertyKey: string) {
  let value: any;

  const getter = function () {
    return value;
  };

  const setter = function (newValue: any) {
    if (!newValue) {
      throw new Error(`${propertyKey} is required`);
    }
    value = newValue;
  };

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

class User {
  @required
  name: string;

  @required
  email: string;
}

// โœ… Good: Validation property decorator
function validate(pattern: RegExp) {
  return function (target: any, propertyKey: string) {
    let value: any;

    const getter = function () {
      return value;
    };

    const setter = function (newValue: any) {
      if (!pattern.test(newValue)) {
        throw new Error(`${propertyKey} is invalid`);
      }
      value = newValue;
    };

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

class User {
  @validate(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
  email: string;
}

Metadata

Using Reflect Metadata

// โœ… Good: Use reflect-metadata
// npm install reflect-metadata
import 'reflect-metadata';

function metadata(key: string, value: any) {
  return function (target: any, propertyKey?: string) {
    if (propertyKey) {
      Reflect.defineMetadata(key, value, target, propertyKey);
    } else {
      Reflect.defineMetadata(key, value, target);
    }
  };
}

@metadata('version', '1.0.0')
class User {
  @metadata('type', 'string')
  name: string;

  @metadata('type', 'number')
  age: number;
}

// Retrieve metadata
const version = Reflect.getMetadata('version', User);
const nameType = Reflect.getMetadata('type', User.prototype, 'name');

Parameter Decorators

// โœ… Good: Parameter decorator
function validate(
  target: any,
  propertyKey: string | symbol | undefined,
  parameterIndex: number
) {
  const existingMetadata = Reflect.getOwnMetadata('validate', target, propertyKey) || [];
  existingMetadata.push(parameterIndex);
  Reflect.defineMetadata('validate', existingMetadata, target, propertyKey);
}

class UserService {
  createUser(@validate name: string, @validate email: string) {
    // Implementation
  }
}

Practical Decorators

Validation Decorator

// โœ… Good: Comprehensive validation decorator
function validateInput(rules: Record<string, any>) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      for (const [key, rule] of Object.entries(rules)) {
        const value = args[0]?.[key];

        if (rule.required && !value) {
          throw new Error(`${key} is required`);
        }

        if (rule.type && typeof value !== rule.type) {
          throw new Error(`${key} must be ${rule.type}`);
        }

        if (rule.pattern && !rule.pattern.test(value)) {
          throw new Error(`${key} is invalid`);
        }
      }

      return originalMethod.apply(this, args);
    };

    return descriptor;
  };
}

class UserService {
  @validateInput({
    name: { required: true, type: 'string' },
    email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ }
  })
  createUser(data: { name: string; email: string }) {
    // Implementation
  }
}

Caching Decorator

// โœ… Good: Caching decorator
function cache(ttl: number = 60000) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;
    const cache = new Map<string, { value: any; timestamp: number }>();

    descriptor.value = function (...args: any[]) {
      const key = JSON.stringify(args);
      const cached = cache.get(key);

      if (cached && Date.now() - cached.timestamp < ttl) {
        console.log(`Cache hit for ${propertyKey}`);
        return cached.value;
      }

      const result = originalMethod.apply(this, args);
      cache.set(key, { value: result, timestamp: Date.now() });
      return result;
    };

    return descriptor;
  };
}

class DataService {
  @cache(5000)
  fetchData(id: number) {
    console.log(`Fetching data for ${id}`);
    return { id, data: 'some data' };
  }
}

Best Practices

  1. Use decorators for cross-cutting concerns:

    // โœ… Good
    @logged
    @timed
    class Service {}
    
    // โŒ Bad
    class Service {
      log() { }
      time() { }
    }
    
  2. Enable experimental decorators:

    // โœ… Good
    "experimentalDecorators": true
    
    // โŒ Bad
    // Decorators not enabled
    
  3. Use metadata for reflection:

    // โœ… Good
    @metadata('version', '1.0')
    class MyClass {}
    
    // โŒ Bad
    class MyClass {
      version = '1.0';
    }
    

Common Mistakes

  1. Not enabling experimental decorators:

    // โŒ Bad
    // tsconfig.json doesn't have experimentalDecorators
    
    // โœ… Good
    "experimentalDecorators": true
    
  2. Modifying prototype incorrectly:

    // โŒ Bad
    function decorator(target: Function) {
      target.prototype.newMethod = () => {};
    }
    
    // โœ… Good
    function decorator(target: Function) {
      const original = target;
      const newConstructor: any = function (...args: any[]) {
        return new original(...args);
      };
      newConstructor.prototype = original.prototype;
      return newConstructor;
    }
    

Summary

Decorators are powerful. Key takeaways:

  • Enable experimental decorators
  • Use class decorators for enhancement
  • Use method decorators for wrapping
  • Use property decorators for validation
  • Use metadata for reflection
  • Apply to cross-cutting concerns
  • Maintain clean code
  • Document decorator behavior

Next Steps

Comments