Skip to main content

Decorators and Metadata in TypeScript

Created: May 8, 2026 Larry Qu 6 min read

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() { }
    }
    ```javascript
    
  2. Enable experimental decorators:
    // ✅ Good
    "experimentalDecorators": true
    
    // ❌ Bad
    // Decorators not enabled
    ```javascript
    
  3. Use metadata for reflection:
    // ✅ Good
    @metadata('version', '1.0')
    class MyClass {}
    
    // ❌ Bad
    class MyClass {
      version = '1.0';
    }
    ```javascript
    

Common Mistakes

  1. Not enabling experimental decorators:
    // ❌ Bad
    // tsconfig.json doesn't have experimentalDecorators
    
    // ✅ Good
    "experimentalDecorators": true
    ```javascript
    
  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

Resources

Comments

Share this article

Scan to read on mobile