JavaScript AdvancedFeatured

JavaScript Decorators: Enhancing Classes and Methods

Master JavaScript decorators for adding metadata and modifying class behavior. Learn decorator syntax, patterns, and practical use cases with TypeScript.

By JavaScript Document Team
decoratorsclassestypescriptadvancedpatterns

Decorators are a stage 3 proposal for JavaScript that enable declarative modification and annotation of classes and their members. They provide a clean syntax for common patterns like logging, validation, memoization, and dependency injection.

Understanding Decorators

Decorators are functions that can modify classes, methods, properties, or parameters at definition time. They use the @ symbol followed by the decorator name.

Basic Decorator Syntax

// TypeScript/experimental JavaScript syntax
@sealed
class BugReport {
  @readonly
  type = "report";

  @validate
  title: string;

  @log
  print() {
    console.log(`Type: ${this.type}`);
    console.log(`Title: ${this.title}`);
  }
}

// Decorator implementations
function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

function readonly(target: any, propertyKey: string) {
  Object.defineProperty(target, propertyKey, {
    writable: false,
    configurable: false
  });
}

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

  descriptor.value = function(...args: any[]) {
    console.log(`Calling ${propertyKey} with arguments:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`${propertyKey} returned:`, result);
    return result;
  };

  return descriptor;
}

function validate(target: any, propertyKey: string) {
  let value = target[propertyKey];

  const getter = () => value;
  const setter = (newVal: string) => {
    if (!newVal || newVal.trim().length === 0) {
      throw new Error(`${propertyKey} cannot be empty`);
    }
    value = newVal;
  };

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

Types of Decorators

Class Decorators

Class decorators are applied to class constructors and can modify or replace the class definition.

// Basic class decorator
function Component(options: ComponentOptions) {
  return function <T extends { new(...args: any[]): {} }>(constructor: T) {
    return class extends constructor {
      selector = options.selector;
      template = options.template;

      constructor(...args: any[]) {
        super(...args);
        console.log(`Component ${options.selector} created`);
      }
    };
  };
}

interface ComponentOptions {
  selector: string;
  template: string;
}

@Component({
  selector: 'app-header',
  template: '<h1>{{title}}</h1>'
})
class HeaderComponent {
  title = 'My App';
}

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

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

@Singleton
class DatabaseConnection {
  private connected = false;

  connect() {
    if (!this.connected) {
      console.log('Connecting to database...');
      this.connected = true;
    }
  }
}

// Multiple decorators
function Injectable(target: Function) {
  target.prototype.injected = true;
}

function Controller(path: string) {
  return function(target: Function) {
    target.prototype.basePath = path;
  };
}

@Injectable
@Controller('/api/users')
class UserController {
  // Controller implementation
}

Method Decorators

Method decorators can modify method behavior, add logging, implement caching, or handle errors.

// Logging decorator
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

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

    try {
      const result = originalMethod.apply(this, args);
      console.log(`[${new Date().toISOString()}] ${propertyKey} returned:`, result);
      return result;
    } catch (error) {
      console.error(`[${new Date().toISOString()}] ${propertyKey} threw:`, error);
      throw error;
    }
  };

  return descriptor;
}

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

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

    if (cache.has(key)) {
      console.log(`Cache hit for ${propertyKey}`);
      return cache.get(key);
    }

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

  return descriptor;
}

// Debounce decorator
function Debounce(delay: number) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    let timeout: NodeJS.Timeout;
    const originalMethod = descriptor.value;

    descriptor.value = function(...args: any[]) {
      clearTimeout(timeout);

      return new Promise((resolve) => {
        timeout = setTimeout(() => {
          resolve(originalMethod.apply(this, args));
        }, delay);
      });
    };

    return descriptor;
  };
}

// Error handling decorator
function CatchError(errorHandler?: (error: Error) => void) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = async function(...args: any[]) {
      try {
        return await originalMethod.apply(this, args);
      } catch (error) {
        if (errorHandler) {
          errorHandler(error);
        } else {
          console.error(`Error in ${propertyKey}:`, error);
        }
        return null;
      }
    };

    return descriptor;
  };
}

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

  @Debounce(1000)
  @Log
  async search(query: string) {
    console.log(`Searching for: ${query}`);
    // Simulate API call
    return `Results for ${query}`;
  }

  @CatchError((error) => console.log('Custom error handler:', error))
  async riskyOperation() {
    throw new Error('Something went wrong!');
  }
}

Property Decorators

Property decorators can add metadata or modify property behavior.

// Validation decorators
const validationRules = new Map<any, Map<string, ValidationRule[]>>();

interface ValidationRule {
  validate: (value: any) => boolean;
  message: string;
}

function Required(target: any, propertyKey: string) {
  addValidationRule(target, propertyKey, {
    validate: (value) => value != null && value !== '',
    message: `${propertyKey} is required`
  });
}

function MinLength(length: number) {
  return function(target: any, propertyKey: string) {
    addValidationRule(target, propertyKey, {
      validate: (value) => value && value.length >= length,
      message: `${propertyKey} must be at least ${length} characters`
    });
  };
}

function MaxLength(length: number) {
  return function(target: any, propertyKey: string) {
    addValidationRule(target, propertyKey, {
      validate: (value) => !value || value.length <= length,
      message: `${propertyKey} must be at most ${length} characters`
    });
  };
}

function Email(target: any, propertyKey: string) {
  addValidationRule(target, propertyKey, {
    validate: (value) => !value || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
    message: `${propertyKey} must be a valid email`
  });
}

function addValidationRule(target: any, propertyKey: string, rule: ValidationRule) {
  if (!validationRules.has(target)) {
    validationRules.set(target, new Map());
  }

  const rules = validationRules.get(target)!;
  if (!rules.has(propertyKey)) {
    rules.set(propertyKey, []);
  }

  rules.get(propertyKey)!.push(rule);
}

// Observable property decorator
function Observable(target: any, propertyKey: string) {
  const privateKey = `_${propertyKey}`;
  const observersKey = `${propertyKey}_observers`;

  target[observersKey] = [];

  Object.defineProperty(target, propertyKey, {
    get() {
      return this[privateKey];
    },
    set(value: any) {
      const oldValue = this[privateKey];
      this[privateKey] = value;

      // Notify observers
      this[observersKey].forEach((observer: Function) => {
        observer.call(this, value, oldValue, propertyKey);
      });
    },
    enumerable: true,
    configurable: true
  });

  // Add subscribe method
  target[`subscribe_${propertyKey}`] = function(observer: Function) {
    this[observersKey].push(observer);

    // Return unsubscribe function
    return () => {
      const index = this[observersKey].indexOf(observer);
      if (index > -1) {
        this[observersKey].splice(index, 1);
      }
    };
  };
}

class User {
  @Required
  @MinLength(3)
  @MaxLength(50)
  username: string;

  @Required
  @Email
  email: string;

  @Observable
  status: 'active' | 'inactive' = 'active';

  validate(): string[] {
    const errors: string[] = [];
    const rules = validationRules.get(Object.getPrototypeOf(this));

    if (rules) {
      rules.forEach((ruleList, property) => {
        const value = (this as any)[property];

        ruleList.forEach(rule => {
          if (!rule.validate(value)) {
            errors.push(rule.message);
          }
        });
      });
    }

    return errors;
  }
}

Parameter Decorators

Parameter decorators can add metadata about method parameters.

// Dependency injection
const injectableMetadata = new Map<any, Map<string, any[]>>();

function Inject(token: any) {
  return function(target: any, propertyKey: string | symbol, parameterIndex: number) {
    if (!injectableMetadata.has(target)) {
      injectableMetadata.set(target, new Map());
    }

    const metadata = injectableMetadata.get(target)!;
    if (!metadata.has(propertyKey as string)) {
      metadata.set(propertyKey as string, []);
    }

    metadata.get(propertyKey as string)![parameterIndex] = token;
  };
}

// Validation parameter decorator
function ValidateParam(validator: (value: any) => boolean, message: string) {
  return function(target: any, propertyKey: string, parameterIndex: number) {
    const originalMethod = target[propertyKey];

    target[propertyKey] = function(...args: any[]) {
      if (!validator(args[parameterIndex])) {
        throw new Error(`Parameter ${parameterIndex} validation failed: ${message}`);
      }

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

// Service tokens
const DATABASE_TOKEN = Symbol('DATABASE');
const LOGGER_TOKEN = Symbol('LOGGER');

class UserService {
  constructor(
    @Inject(DATABASE_TOKEN) private database: any,
    @Inject(LOGGER_TOKEN) private logger: any
  ) {}

  findUser(
    @ValidateParam((id) => typeof id === 'number' && id > 0, 'ID must be positive number')
    id: number
  ) {
    this.logger.log(`Finding user ${id}`);
    return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
  }
}

Advanced Decorator Patterns

Decorator Factories

// Configurable decorator factory
function Retry(options: RetryOptions = {}) {
  const {
    attempts = 3,
    delay = 1000,
    backoff = 2,
    onError
  } = options;

  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 (error) {
          lastError = error;

          if (onError) {
            onError(error, i + 1);
          }

          if (i < attempts - 1) {
            const waitTime = delay * Math.pow(backoff, i);
            await new Promise(resolve => setTimeout(resolve, waitTime));
          }
        }
      }

      throw lastError!;
    };

    return descriptor;
  };
}

interface RetryOptions {
  attempts?: number;
  delay?: number;
  backoff?: number;
  onError?: (error: Error, attempt: number) => void;
}

// Rate limiting decorator
function RateLimit(options: RateLimitOptions) {
  const { limit, window } = options;
  const calls = new Map<string, number[]>();

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

    descriptor.value = function(...args: any[]) {
      const key = `${this.constructor.name}.${propertyKey}`;
      const now = Date.now();

      if (!calls.has(key)) {
        calls.set(key, []);
      }

      const timestamps = calls.get(key)!;
      // Remove old timestamps
      const cutoff = now - window;
      calls.set(key, timestamps.filter(t => t > cutoff));

      if (timestamps.length >= limit) {
        throw new Error(`Rate limit exceeded for ${key}`);
      }

      timestamps.push(now);
      return originalMethod.apply(this, args);
    };

    return descriptor;
  };
}

interface RateLimitOptions {
  limit: number;
  window: number; // milliseconds
}

// Conditional decorator
function When(condition: (target: any) => boolean) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function(...args: any[]) {
      if (!condition(this)) {
        throw new Error(`Condition not met for ${propertyKey}`);
      }

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

    return descriptor;
  };
}

class ApiService {
  private isAuthenticated = false;

  @Retry({
    attempts: 3,
    delay: 500,
    onError: (error, attempt) => console.log(`Attempt ${attempt} failed:`, error)
  })
  async fetchData(url: string) {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return response.json();
  }

  @RateLimit({ limit: 10, window: 60000 }) // 10 calls per minute
  @When((instance) => instance.isAuthenticated)
  async postData(data: any) {
    console.log('Posting data:', data);
    // API call implementation
  }
}

Metadata and Reflection

// Metadata decorator system
const metadata = new WeakMap<any, Map<string | symbol, any>>();

function SetMetadata(key: string, value: any) {
  return function(target: any, propertyKey?: string | symbol) {
    if (!metadata.has(target)) {
      metadata.set(target, new Map());
    }

    const metaKey = propertyKey || 'class';
    const meta = metadata.get(target)!;

    if (!meta.has(metaKey)) {
      meta.set(metaKey, {});
    }

    meta.get(metaKey)[key] = value;
  };
}

function GetMetadata(target: any, propertyKey?: string | symbol, key?: string) {
  const meta = metadata.get(target);
  if (!meta) return undefined;

  const metaKey = propertyKey || 'class';
  const propMeta = meta.get(metaKey);

  return key ? propMeta?.[key] : propMeta;
}

// HTTP method decorators
function HttpMethod(method: string, path: string) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    SetMetadata('method', method)(target, propertyKey);
    SetMetadata('path', path)(target, propertyKey);
    return descriptor;
  };
}

const Get = (path: string) => HttpMethod('GET', path);
const Post = (path: string) => HttpMethod('POST', path);
const Put = (path: string) => HttpMethod('PUT', path);
const Delete = (path: string) => HttpMethod('DELETE', path);

// Route controller
@SetMetadata('basePath', '/api/products')
class ProductController {
  @Get('/')
  @SetMetadata('roles', ['user', 'admin'])
  async getProducts() {
    return [{ id: 1, name: 'Product 1' }];
  }

  @Get('/:id')
  @SetMetadata('roles', ['user', 'admin'])
  async getProduct(id: string) {
    return { id, name: `Product ${id}` };
  }

  @Post('/')
  @SetMetadata('roles', ['admin'])
  async createProduct(data: any) {
    return { id: Date.now(), ...data };
  }

  @Put('/:id')
  @SetMetadata('roles', ['admin'])
  async updateProduct(id: string, data: any) {
    return { id, ...data };
  }

  @Delete('/:id')
  @SetMetadata('roles', ['admin'])
  async deleteProduct(id: string) {
    return { deleted: true, id };
  }
}

// Router registration
function registerRoutes(controller: any) {
  const basePath = GetMetadata(controller, undefined, 'basePath') || '';
  const prototype = controller.prototype;

  Object.getOwnPropertyNames(prototype).forEach(propertyKey => {
    if (propertyKey === 'constructor') return;

    const method = GetMetadata(prototype, propertyKey, 'method');
    const path = GetMetadata(prototype, propertyKey, 'path');
    const roles = GetMetadata(prototype, propertyKey, 'roles');

    if (method && path) {
      console.log(`Registering ${method} ${basePath}${path}`);
      console.log(`Required roles:`, roles);
      // Register with actual router here
    }
  });
}

registerRoutes(ProductController);

Composition and Stacking

// Compose decorators
function compose(...decorators: MethodDecorator[]): MethodDecorator {
  return function(target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) {
    decorators.reverse().forEach(decorator => {
      descriptor = decorator(target, propertyKey, descriptor) || descriptor;
    });
    return descriptor;
  };
}

// Timing decorator
function Time(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = async function(...args: any[]) {
    const start = performance.now();

    try {
      const result = await originalMethod.apply(this, args);
      const duration = performance.now() - start;
      console.log(`${propertyKey} took ${duration.toFixed(2)}ms`);
      return result;
    } catch (error) {
      const duration = performance.now() - start;
      console.log(`${propertyKey} failed after ${duration.toFixed(2)}ms`);
      throw error;
    }
  };

  return descriptor;
}

// Cache with TTL
function CacheWithTTL(ttl: number) {
  const cache = new Map<string, { value: any; expires: number }>();

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

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

      if (cached && cached.expires > Date.now()) {
        console.log(`Cache hit for ${propertyKey}`);
        return cached.value;
      }

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

      return result;
    };

    return descriptor;
  };
}

// Transform result decorator
function Transform(transformer: (result: any) => any) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = async function(...args: any[]) {
      const result = await originalMethod.apply(this, args);
      return transformer(result);
    };

    return descriptor;
  };
}

class DataService {
  @compose(
    Time,
    CacheWithTTL(60000),
    Transform(result => ({ data: result, timestamp: Date.now() })),
    Log
  )
  async fetchUserData(userId: number) {
    // Simulate API call
    await new Promise(resolve => setTimeout(resolve, 1000));
    return { id: userId, name: `User ${userId}` };
  }
}

Real-World Examples

Dependency Injection Container

// Simple DI container
class Container {
  private services = new Map<any, any>();
  private factories = new Map<any, () => any>();

  register<T>(token: any, factory: () => T): void {
    this.factories.set(token, factory);
  }

  registerSingleton<T>(token: any, instance: T): void {
    this.services.set(token, instance);
  }

  resolve<T>(token: any): T {
    if (this.services.has(token)) {
      return this.services.get(token);
    }

    if (this.factories.has(token)) {
      const instance = this.factories.get(token)!();
      this.services.set(token, instance);
      return instance;
    }

    throw new Error(`No provider for ${token.toString()}`);
  }
}

const container = new Container();

// Injectable decorator
function Injectable(target: any) {
  const paramTypes = Reflect.getMetadata('design:paramtypes', target) || [];
  const tokens = injectableMetadata.get(target)?.get('constructor') || [];

  container.register(target, () => {
    const deps = paramTypes.map((type: any, index: number) => {
      const token = tokens[index] || type;
      return container.resolve(token);
    });

    return new target(...deps);
  });
}

// Example services
interface IDatabase {
  query(sql: string): any[];
}

interface ILogger {
  log(message: string): void;
}

@Injectable
class ConsoleLogger implements ILogger {
  log(message: string): void {
    console.log(`[${new Date().toISOString()}] ${message}`);
  }
}

@Injectable
class MockDatabase implements IDatabase {
  constructor(@Inject('LOGGER') private logger: ILogger) {}

  query(sql: string): any[] {
    this.logger.log(`Executing query: ${sql}`);
    return [];
  }
}

// Register services
container.registerSingleton('LOGGER', new ConsoleLogger());
container.register('DATABASE', () => container.resolve(MockDatabase));

// Usage
const db = container.resolve<IDatabase>('DATABASE');
db.query('SELECT * FROM users');

ORM-like Decorators

// Entity decorators
const entityMetadata = new Map<any, EntityMetadata>();

interface EntityMetadata {
  tableName: string;
  columns: Map<string, ColumnMetadata>;
}

interface ColumnMetadata {
  name: string;
  type: string;
  primaryKey?: boolean;
  nullable?: boolean;
  default?: any;
}

function Entity(tableName: string) {
  return function(target: any) {
    entityMetadata.set(target, {
      tableName,
      columns: new Map()
    });
  };
}

function Column(options: Partial<ColumnMetadata> = {}) {
  return function(target: any, propertyKey: string) {
    const constructor = target.constructor;

    if (!entityMetadata.has(constructor)) {
      entityMetadata.set(constructor, {
        tableName: constructor.name.toLowerCase(),
        columns: new Map()
      });
    }

    const metadata = entityMetadata.get(constructor)!;
    metadata.columns.set(propertyKey, {
      name: options.name || propertyKey,
      type: options.type || 'VARCHAR(255)',
      ...options
    });
  };
}

function PrimaryKey(target: any, propertyKey: string) {
  Column({ primaryKey: true, type: 'INTEGER' })(target, propertyKey);
}

function CreatedAt(target: any, propertyKey: string) {
  Column({ type: 'TIMESTAMP', default: 'CURRENT_TIMESTAMP' })(target, propertyKey);
}

function UpdatedAt(target: any, propertyKey: string) {
  Column({
    type: 'TIMESTAMP',
    default: 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'
  })(target, propertyKey);
}

// Example entity
@Entity('users')
class User {
  @PrimaryKey
  id: number;

  @Column({ type: 'VARCHAR(100)', nullable: false })
  username: string;

  @Column({ type: 'VARCHAR(255)', nullable: false })
  email: string;

  @Column({ type: 'TEXT', nullable: true })
  bio?: string;

  @CreatedAt
  createdAt: Date;

  @UpdatedAt
  updatedAt: Date;
}

// Generate SQL from decorators
function generateCreateTableSQL(entity: any): string {
  const metadata = entityMetadata.get(entity);
  if (!metadata) {
    throw new Error('Entity metadata not found');
  }

  const columns: string[] = [];
  let primaryKey: string | undefined;

  metadata.columns.forEach((column, property) => {
    let sql = `${column.name} ${column.type}`;

    if (column.primaryKey) {
      sql += ' PRIMARY KEY AUTO_INCREMENT';
      primaryKey = column.name;
    }

    if (column.nullable === false) {
      sql += ' NOT NULL';
    }

    if (column.default !== undefined) {
      sql += ` DEFAULT ${column.default}`;
    }

    columns.push(sql);
  });

  return `CREATE TABLE ${metadata.tableName} (\n  ${columns.join(',\n  ')}\n);`;
}

console.log(generateCreateTableSQL(User));

Validation Framework

// Comprehensive validation system
class Validator {
  private static schemas = new Map<any, ValidationSchema>();

  static addRule(target: any, property: string, rule: ValidationRule) {
    if (!this.schemas.has(target)) {
      this.schemas.set(target, { rules: new Map() });
    }

    const schema = this.schemas.get(target)!;
    if (!schema.rules.has(property)) {
      schema.rules.set(property, []);
    }

    schema.rules.get(property)!.push(rule);
  }

  static validate(instance: any): ValidationResult {
    const errors: ValidationError[] = [];
    const constructor = Object.getPrototypeOf(instance);
    const schema = this.schemas.get(constructor);

    if (!schema) {
      return { valid: true, errors: [] };
    }

    schema.rules.forEach((rules, property) => {
      const value = instance[property];

      rules.forEach(rule => {
        if (!rule.validate(value, instance)) {
          errors.push({
            property,
            value,
            message: typeof rule.message === 'function'
              ? rule.message(property, value)
              : rule.message
          });
        }
      });
    });

    return {
      valid: errors.length === 0,
      errors
    };
  }
}

interface ValidationSchema {
  rules: Map<string, ValidationRule[]>;
}

interface ValidationResult {
  valid: boolean;
  errors: ValidationError[];
}

interface ValidationError {
  property: string;
  value: any;
  message: string;
}

// Validation decorators
function IsRequired(message?: string | ((prop: string) => string)) {
  return function(target: any, property: string) {
    Validator.addRule(target, property, {
      validate: (value) => value !== null && value !== undefined && value !== '',
      message: message || `${property} is required`
    });
  };
}

function IsEmail(message?: string) {
  return function(target: any, property: string) {
    Validator.addRule(target, property, {
      validate: (value) => {
        if (!value) return true; // Skip if empty
        return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
      },
      message: message || `${property} must be a valid email address`
    });
  };
}

function MinValue(min: number, message?: string) {
  return function(target: any, property: string) {
    Validator.addRule(target, property, {
      validate: (value) => value == null || value >= min,
      message: message || `${property} must be at least ${min}`
    });
  };
}

function MaxValue(max: number, message?: string) {
  return function(target: any, property: string) {
    Validator.addRule(target, property, {
      validate: (value) => value == null || value <= max,
      message: message || `${property} must be at most ${max}`
    });
  };
}

function Pattern(pattern: RegExp, message?: string) {
  return function(target: any, property: string) {
    Validator.addRule(target, property, {
      validate: (value) => !value || pattern.test(value),
      message: message || `${property} does not match required pattern`
    });
  };
}

function Custom(validator: (value: any, instance: any) => boolean, message: string) {
  return function(target: any, property: string) {
    Validator.addRule(target, property, {
      validate: validator,
      message
    });
  };
}

// Example usage
class RegistrationForm {
  @IsRequired()
  @MinLength(3)
  @MaxLength(20)
  @Pattern(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores')
  username: string;

  @IsRequired()
  @IsEmail()
  email: string;

  @IsRequired()
  @MinLength(8)
  @Pattern(/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, 'Password must contain lowercase, uppercase, and number')
  password: string;

  @IsRequired()
  @Custom((value, instance) => value === instance.password, 'Passwords must match')
  confirmPassword: string;

  @IsRequired()
  @MinValue(18, 'You must be at least 18 years old')
  @MaxValue(120, 'Please enter a valid age')
  age: number;

  @Custom((value) => value === true, 'You must accept the terms and conditions')
  acceptTerms: boolean;
}

// Validate instance
const form = new RegistrationForm();
form.username = 'john_doe';
form.email = 'invalid-email';
form.password = 'weak';
form.confirmPassword = 'different';
form.age = 16;
form.acceptTerms = false;

const result = Validator.validate(form);
console.log('Valid:', result.valid);
console.log('Errors:', result.errors);

Best Practices

  1. Keep decorators simple and focused
// Good - single responsibility
@Log
@Validate
@Cache
method() { }

// Bad - doing too much
@LogValidateCacheAndRetry
method() { }
  1. Use decorator factories for configuration
// Good - configurable
@Retry({ attempts: 3, delay: 1000 })
@Cache({ ttl: 60000 })
method() { }

// Less flexible
@RetryThreeTimes
@CacheOneMinute
method() { }
  1. Preserve method signatures
// Good - preserves original signature
function Log(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args: any[]) {
    console.log(`Calling ${key}`);
    return original.apply(this, args);
  };
  return descriptor;
}
  1. Handle errors gracefully
// Good - proper error handling
function SafeExecute(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = async function(...args: any[]) {
    try {
      return await original.apply(this, args);
    } catch (error) {
      console.error(`Error in ${key}:`, error);
      // Handle or re-throw as appropriate
      throw error;
    }
  };
  return descriptor;
}

Conclusion

Decorators provide a powerful way to add behavior to classes and their members in a declarative manner. They enable cleaner code organization, better separation of concerns, and reusable cross-cutting functionality. While still a proposal for JavaScript, decorators are widely used in TypeScript and form the foundation of many modern frameworks. Understanding decorators is essential for advanced JavaScript development and building maintainable, scalable applications.