+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 54 of 354

🎯 Method and Property Decorators: Fine-Grained Control

Master method and property decorators in TypeScript to add precise behavior modifications and metadata at the member level 🚀

💎Advanced
30 min read

Prerequisites

  • Understanding of decorators fundamentals 📝
  • Knowledge of property descriptors 🔍
  • Familiarity with class member concepts 💻

What you'll learn

  • Create powerful method decorators 🎯
  • Implement property decorators with getters/setters 🏗️
  • Build validation and transformation decorators 🛡️
  • Master parameter decorators for dependency injection ✨

🎯 Introduction

Welcome to the precision world of method and property decorators! 🎉 In this guide, we’ll explore how to use decorators at the member level to add fine-grained control over individual methods and properties.

You’ll discover how method and property decorators are like surgical tools 🔧 - they allow you to make precise modifications exactly where needed! Whether you’re implementing logging 📊, validation ✅, or performance monitoring ⏱️, these decorators provide elegant solutions at the member level.

By the end of this tutorial, you’ll be confidently creating decorators that enhance your class members with powerful capabilities! Let’s dive into the details! 🏊‍♂️

📚 Understanding Member Decorators

🤔 Method vs Property Decorators

Member decorators work at a more granular level than class decorators:

  • Method Decorators: Receive the property descriptor and can modify method behavior
  • Property Decorators: Work with property definitions but have limited descriptor access
  • Accessor Decorators: Applied to getters/setters with full descriptor control
  • Parameter Decorators: Mark parameters for metadata purposes

Think of member decorators like:

  • 🎯 Precision tools: Targeting specific functionality
  • 🔬 Microscopes: Working at the detailed level
  • 🎨 Fine brushes: Adding details to the bigger picture
  • 🧰 Specialized tools: Each with a specific purpose

💡 Decorator Signatures

// Method decorator
type MethodDecorator = (
  target: any,
  propertyKey: string | symbol,
  descriptor: PropertyDescriptor
) => PropertyDescriptor | void;

// Property decorator
type PropertyDecorator = (
  target: any,
  propertyKey: string | symbol
) => void;

// Parameter decorator
type ParameterDecorator = (
  target: any,
  propertyKey: string | symbol,
  parameterIndex: number
) => void;

🔧 Method Decorators

📝 Basic Method Enhancement

Let’s start with fundamental method decorator patterns:

// 🎯 Logging 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 start = performance.now();
    
    try {
      const result = originalMethod.apply(this, args);
      const end = performance.now();
      console.log(`✅ ${propertyKey} completed in ${(end - start).toFixed(2)}ms`);
      return result;
    } catch (error) {
      console.error(`❌ ${propertyKey} failed:`, error);
      throw error;
    }
  };
  
  return descriptor;
}

// 🔄 Retry decorator
function retry(attempts: 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 < attempts; i++) {
        try {
          return await originalMethod.apply(this, args);
        } catch (error) {
          if (i === attempts - 1) throw error;
          
          console.log(`🔄 Retry ${i + 1}/${attempts} for ${propertyKey}`);
          await new Promise(resolve => setTimeout(resolve, delay));
        }
      }
    };
    
    return descriptor;
  };
}

// 🔒 Method validation decorator
function validate(validator: (...args: any[]) => boolean, message?: string) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = function(...args: any[]) {
      if (!validator.apply(this, args)) {
        throw new Error(message || `Validation failed for ${propertyKey}`);
      }
      
      return originalMethod.apply(this, args);
    };
    
    return descriptor;
  };
}

// 🕐 Debounce decorator
function debounce(delay: number) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    let timeoutId: any;
    const originalMethod = descriptor.value;
    
    descriptor.value = function(...args: any[]) {
      clearTimeout(timeoutId);
      
      return new Promise((resolve) => {
        timeoutId = setTimeout(() => {
          resolve(originalMethod.apply(this, args));
        }, delay);
      });
    };
    
    return descriptor;
  };
}

// 🏠 Example usage
class DataService {
  @log
  @retry(3, 500)
  async fetchData(id: string): Promise<any> {
    // Simulate API call that might fail
    if (Math.random() > 0.7) {
      throw new Error('Network error');
    }
    
    return { id, data: 'Success!' };
  }
  
  @validate(
    (amount: number) => amount > 0 && amount <= 1000,
    'Amount must be between 0 and 1000'
  )
  processPayment(amount: number): void {
    console.log(`💰 Processing payment of $${amount}`);
  }
  
  @debounce(300)
  async search(query: string): Promise<string[]> {
    console.log(`🔍 Searching for: ${query}`);
    return [`Result for ${query}`];
  }
}

// 💫 Testing
const service = new DataService();

// Test retry with logging
service.fetchData('123').then(console.log).catch(console.error);

// Test validation
try {
  service.processPayment(500); // Valid
  service.processPayment(1500); // Will throw
} catch (e) {
  console.error(e.message);
}

// Test debounce
service.search('test'); // Will be cancelled
service.search('test2'); // Will be cancelled
service.search('test3'); // Will execute after 300ms

🚀 Advanced Method Decorators

Building more sophisticated method decorators:

// 🎯 Memoization decorator
function memoize(
  keyGenerator?: (...args: any[]) => string,
  ttl?: number
) {
  const cache = new Map<string, { value: any; timestamp: number }>();
  
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = function(...args: any[]) {
      const key = keyGenerator ? keyGenerator(...args) : JSON.stringify(args);
      const cached = cache.get(key);
      
      if (cached) {
        if (!ttl || 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;
    };
    
    // Add cache management methods
    descriptor.value.clearCache = () => cache.clear();
    descriptor.value.getCacheSize = () => cache.size;
    
    return descriptor;
  };
}

// 🔐 Role-based access control
function requireRole(role: string | string[]) {
  const roles = Array.isArray(role) ? role : [role];
  
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = function(...args: any[]) {
      const userRoles = (this as any).currentUser?.roles || [];
      const hasRole = roles.some(r => userRoles.includes(r));
      
      if (!hasRole) {
        throw new Error(`Access denied. Required roles: ${roles.join(', ')}`);
      }
      
      return originalMethod.apply(this, args);
    };
    
    return descriptor;
  };
}

// 📊 Performance monitoring with detailed metrics
interface PerformanceMetrics {
  count: number;
  totalTime: number;
  avgTime: number;
  minTime: number;
  maxTime: number;
  lastCall: Date;
}

function monitor() {
  const metrics = new Map<string, PerformanceMetrics>();
  
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    const key = `${target.constructor.name}.${propertyKey}`;
    
    descriptor.value = function(...args: any[]) {
      const start = performance.now();
      
      try {
        const result = originalMethod.apply(this, args);
        const duration = performance.now() - start;
        
        updateMetrics(key, duration);
        
        return result;
      } catch (error) {
        const duration = performance.now() - start;
        updateMetrics(key, duration);
        throw error;
      }
    };
    
    function updateMetrics(key: string, duration: number) {
      const current = metrics.get(key) || {
        count: 0,
        totalTime: 0,
        avgTime: 0,
        minTime: Infinity,
        maxTime: 0,
        lastCall: new Date()
      };
      
      current.count++;
      current.totalTime += duration;
      current.avgTime = current.totalTime / current.count;
      current.minTime = Math.min(current.minTime, duration);
      current.maxTime = Math.max(current.maxTime, duration);
      current.lastCall = new Date();
      
      metrics.set(key, current);
    }
    
    // Add metrics access
    descriptor.value.getMetrics = () => metrics.get(key);
    (target.constructor as any).getAllMetrics = () => 
      Object.fromEntries(metrics);
    
    return descriptor;
  };
}

// 🏠 Advanced example
class UserService {
  currentUser = { roles: ['user', 'admin'] };
  
  @memoize(
    (userId: string) => userId,
    60000 // 1 minute TTL
  )
  getUserById(userId: string): any {
    console.log(`📡 Fetching user ${userId} from database`);
    return { id: userId, name: `User ${userId}` };
  }
  
  @requireRole('admin')
  @monitor()
  deleteUser(userId: string): void {
    console.log(`🗑️ Deleting user ${userId}`);
  }
  
  @requireRole(['admin', 'moderator'])
  @monitor()
  suspendUser(userId: string, reason: string): void {
    console.log(`⛔ Suspending user ${userId}: ${reason}`);
  }
}

// 💫 Testing
const userService = new UserService();

// Test memoization
console.log(userService.getUserById('123')); // Fetches
console.log(userService.getUserById('123')); // From cache
console.log(`Cache size: ${(userService.getUserById as any).getCacheSize()}`);

// Test role-based access
userService.deleteUser('456'); // Works (user has admin role)

// Test monitoring
userService.suspendUser('789', 'Spam');
console.log('Metrics:', (userService.suspendUser as any).getMetrics());

🎨 Property Decorators

📝 Basic Property Enhancement

Property decorators for validation and transformation:

// 🎯 Property validation decorator
function validateProperty(validator: (value: any) => boolean, message?: string) {
  return function(target: any, propertyKey: string) {
    let value: any;
    
    const getter = function() {
      return value;
    };
    
    const setter = function(newVal: any) {
      if (!validator(newVal)) {
        throw new Error(message || `Invalid value for ${propertyKey}: ${newVal}`);
      }
      value = newVal;
    };
    
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
  };
}

// 🔄 Transform decorator
function transform(transformer: (value: any) => any) {
  return function(target: any, propertyKey: string) {
    let value: any;
    
    const getter = function() {
      return value;
    };
    
    const setter = function(newVal: any) {
      value = transformer(newVal);
    };
    
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
  };
}

// 🔍 Observable property decorator
function observable(target: any, propertyKey: string) {
  let value: any;
  const observers: ((newValue: any, oldValue: any) => void)[] = [];
  
  const getter = function() {
    return value;
  };
  
  const setter = function(newVal: any) {
    const oldVal = value;
    value = newVal;
    
    // Notify observers
    observers.forEach(observer => observer(newVal, oldVal));
  };
  
  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true
  });
  
  // Add observer methods
  target[`observe${propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1)}`] = 
    function(callback: (newValue: any, oldValue: any) => void) {
      observers.push(callback);
      return () => {
        const index = observers.indexOf(callback);
        if (index > -1) observers.splice(index, 1);
      };
    };
}

// 💾 Persistent property decorator
function persistent(storageKey?: string) {
  return function(target: any, propertyKey: string) {
    const key = storageKey || `${target.constructor.name}.${propertyKey}`;
    let value: any;
    
    // Load from storage on first access
    const getter = function() {
      if (value === undefined) {
        const stored = localStorage.getItem(key);
        if (stored) {
          value = JSON.parse(stored);
        }
      }
      return value;
    };
    
    const setter = function(newVal: any) {
      value = newVal;
      localStorage.setItem(key, JSON.stringify(newVal));
    };
    
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
  };
}

// 🏠 Example usage
class Configuration {
  @validateProperty(
    (v: number) => v >= 0 && v <= 100,
    'Volume must be between 0 and 100'
  )
  volume!: number;
  
  @transform((v: string) => v.trim().toLowerCase())
  username!: string;
  
  @observable
  theme!: 'light' | 'dark';
  
  @persistent('app.language')
  language: string = 'en';
  
  constructor() {
    // Set up theme observer
    this.observeTheme((newTheme, oldTheme) => {
      console.log(`🎨 Theme changed from ${oldTheme} to ${newTheme}`);
    });
  }
}

// 💫 Testing
const config = new Configuration();

// Test validation
config.volume = 50; // OK
try {
  config.volume = 150; // Error
} catch (e) {
  console.error(e.message);
}

// Test transformation
config.username = '  JOHN_DOE  ';
console.log('Username:', config.username); // 'john_doe'

// Test observable
config.theme = 'dark'; // Triggers observer

// Test persistence
config.language = 'es';
console.log('Stored language:', localStorage.getItem('app.language'));

🚀 Advanced Property Patterns

Complex property decorator implementations:

// 🎯 Computed property decorator
interface ComputedOptions<T> {
  get: () => T;
  set?: (value: T) => void;
  cache?: boolean;
}

function computed<T>(options: ComputedOptions<T> | (() => T)) {
  const config: ComputedOptions<T> = typeof options === 'function' 
    ? { get: options } 
    : options;
  
  return function(target: any, propertyKey: string) {
    let cachedValue: T;
    let isCached = false;
    
    const getter = function() {
      if (config.cache && isCached) {
        return cachedValue;
      }
      
      const value = config.get.call(this);
      
      if (config.cache) {
        cachedValue = value;
        isCached = true;
      }
      
      return value;
    };
    
    const setter = config.set ? function(value: T) {
      config.set!.call(this, value);
      isCached = false; // Invalidate cache
    } : undefined;
    
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
  };
}

// 🔒 Access control decorator
interface AccessControlOptions {
  read?: string[];
  write?: string[];
}

function accessControl(options: AccessControlOptions) {
  return function(target: any, propertyKey: string) {
    let value: any;
    
    const checkAccess = (operation: 'read' | 'write') => {
      const requiredRoles = options[operation];
      if (!requiredRoles || requiredRoles.length === 0) return true;
      
      const userRoles = (this as any).currentUser?.roles || [];
      return requiredRoles.some(role => userRoles.includes(role));
    };
    
    const getter = function() {
      if (!checkAccess.call(this, 'read')) {
        throw new Error(`Read access denied for ${propertyKey}`);
      }
      return value;
    };
    
    const setter = function(newVal: any) {
      if (!checkAccess.call(this, 'write')) {
        throw new Error(`Write access denied for ${propertyKey}`);
      }
      value = newVal;
    };
    
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
  };
}

// 📊 Property history decorator
interface HistoryEntry<T> {
  value: T;
  timestamp: Date;
  user?: string;
}

function trackHistory<T>(maxEntries: number = 10) {
  return function(target: any, propertyKey: string) {
    const historyKey = `_${propertyKey}_history`;
    let currentValue: T;
    
    // Initialize history array
    target[historyKey] = [];
    
    const getter = function() {
      return currentValue;
    };
    
    const setter = function(newVal: T) {
      const history: HistoryEntry<T>[] = this[historyKey];
      
      // Add to history
      history.push({
        value: currentValue,
        timestamp: new Date(),
        user: (this as any).currentUser?.name
      });
      
      // Limit history size
      if (history.length > maxEntries) {
        history.shift();
      }
      
      currentValue = newVal;
    };
    
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
    
    // Add history access method
    target[`get${propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1)}History`] = 
      function() {
        return [...this[historyKey]];
      };
  };
}

// 🏠 Advanced example
class Document {
  currentUser = { name: 'John', roles: ['editor'] };
  
  @trackHistory(5)
  content: string = '';
  
  @computed({
    get() {
      return this.content.split(/\s+/).length;
    },
    cache: true
  })
  wordCount!: number;
  
  @computed(() => new Date().toISOString())
  lastAccessed!: string;
  
  @accessControl({
    read: ['viewer', 'editor', 'admin'],
    write: ['editor', 'admin']
  })
  title: string = 'Untitled';
  
  updateContent(text: string) {
    this.content = text;
  }
}

// 💫 Testing
const doc = new Document();

// Test history tracking
doc.content = 'Hello world';
doc.content = 'Hello TypeScript';
doc.content = 'Hello decorators';
console.log('Content history:', doc.getContentHistory());

// Test computed properties
console.log('Word count:', doc.wordCount); // Computed
console.log('Last accessed:', doc.lastAccessed); // Always current

// Test access control
try {
  doc.title = 'New Title'; // Works (user has editor role)
  
  // Change user role
  doc.currentUser.roles = ['viewer'];
  doc.title = 'Another Title'; // Will throw
} catch (e) {
  console.error(e.message);
}

🎯 Parameter Decorators

📝 Parameter Metadata and Validation

Using parameter decorators for validation and dependency injection:

import 'reflect-metadata';

// 🎯 Parameter validation
const VALIDATORS_KEY = Symbol('validators');

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

function Required(target: any, propertyKey: string, parameterIndex: number) {
  const validators: Map<number, Validator[]> = 
    Reflect.getOwnMetadata(VALIDATORS_KEY, target, propertyKey) || new Map();
  
  const paramValidators = validators.get(parameterIndex) || [];
  paramValidators.push({
    validate: (value) => value !== undefined && value !== null,
    message: `Parameter ${parameterIndex} is required`
  });
  
  validators.set(parameterIndex, paramValidators);
  Reflect.defineMetadata(VALIDATORS_KEY, validators, target, propertyKey);
}

function Min(min: number) {
  return function(target: any, propertyKey: string, parameterIndex: number) {
    const validators: Map<number, Validator[]> = 
      Reflect.getOwnMetadata(VALIDATORS_KEY, target, propertyKey) || new Map();
    
    const paramValidators = validators.get(parameterIndex) || [];
    paramValidators.push({
      validate: (value) => value >= min,
      message: `Parameter ${parameterIndex} must be at least ${min}`
    });
    
    validators.set(parameterIndex, paramValidators);
    Reflect.defineMetadata(VALIDATORS_KEY, validators, target, propertyKey);
  };
}

function ValidateParams(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function(...args: any[]) {
    const validators: Map<number, Validator[]> = 
      Reflect.getOwnMetadata(VALIDATORS_KEY, target, propertyKey) || new Map();
    
    // Validate each parameter
    validators.forEach((paramValidators, index) => {
      const value = args[index];
      
      paramValidators.forEach(validator => {
        if (!validator.validate(value)) {
          throw new Error(`${propertyKey}: ${validator.message} (got ${value})`);
        }
      });
    });
    
    return originalMethod.apply(this, args);
  };
  
  return descriptor;
}

// 🔧 Dependency injection
const INJECT_KEY = Symbol('inject');
const container = new Map<string, any>();

function Inject(token: string) {
  return function(target: any, propertyKey: string, parameterIndex: number) {
    const injections = Reflect.getOwnMetadata(INJECT_KEY, target, propertyKey) || new Map();
    injections.set(parameterIndex, token);
    Reflect.defineMetadata(INJECT_KEY, injections, target, propertyKey);
  };
}

function ResolveParams(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function(...args: any[]) {
    const injections: Map<number, string> = 
      Reflect.getOwnMetadata(INJECT_KEY, target, propertyKey) || new Map();
    
    // Resolve injected parameters
    injections.forEach((token, index) => {
      if (args[index] === undefined) {
        args[index] = container.get(token);
      }
    });
    
    return originalMethod.apply(this, args);
  };
  
  return descriptor;
}

// 🏠 Example usage
class PaymentService {
  @ValidateParams
  processPayment(
    @Required @Min(0.01) amount: number,
    @Required currency: string,
    description?: string
  ): void {
    console.log(`💳 Processing ${currency} ${amount} - ${description || 'No description'}`);
  }
  
  @ResolveParams
  sendNotification(
    message: string,
    @Inject('emailService') emailService?: any,
    @Inject('smsService') smsService?: any
  ): void {
    console.log('📬 Sending notification:', message);
    emailService?.send(message);
    smsService?.send(message);
  }
}

// Set up services
container.set('emailService', {
  send: (msg: string) => console.log(`📧 Email: ${msg}`)
});
container.set('smsService', {
  send: (msg: string) => console.log(`📱 SMS: ${msg}`)
});

// 💫 Testing
const payment = new PaymentService();

// Test validation
payment.processPayment(100, 'USD', 'Test payment'); // OK
try {
  payment.processPayment(0, 'USD'); // Error: too small
} catch (e) {
  console.error(e.message);
}

// Test dependency injection
payment.sendNotification('Payment confirmed'); // Services injected automatically

🎪 Combining Decorators

🔄 Decorator Composition Patterns

Creating powerful combinations of decorators:

// 🎯 Transaction decorator
function transactional(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = async function(...args: any[]) {
    console.log('🔄 Starting transaction');
    const transaction = { id: Date.now(), rollback: [] as Function[] };
    
    try {
      const result = await originalMethod.apply(this, args);
      console.log('✅ Committing transaction');
      return result;
    } catch (error) {
      console.log('❌ Rolling back transaction');
      transaction.rollback.reverse().forEach(fn => fn());
      throw error;
    }
  };
  
  return descriptor;
}

// 🔐 Authenticated decorator
function authenticated(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function(...args: any[]) {
    if (!(this as any).isAuthenticated?.()) {
      throw new Error('Authentication required');
    }
    
    return originalMethod.apply(this, args);
  };
  
  return descriptor;
}

// 📊 Audit decorator
function audit(action: string) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = function(...args: any[]) {
      const auditLog = {
        action,
        method: propertyKey,
        user: (this as any).currentUser,
        timestamp: new Date(),
        args: args
      };
      
      console.log('📋 Audit:', auditLog);
      
      return originalMethod.apply(this, args);
    };
    
    return descriptor;
  };
}

// 🏠 Combined example
class BankingService {
  currentUser = { id: '123', name: 'John' };
  
  isAuthenticated() {
    return !!this.currentUser;
  }
  
  @transactional
  @authenticated
  @audit('TRANSFER_MONEY')
  @validate((from: string, to: string, amount: number) => amount > 0)
  async transferMoney(from: string, to: string, amount: number): Promise<void> {
    console.log(`💸 Transferring ${amount} from ${from} to ${to}`);
    
    // Simulate operation that might fail
    if (Math.random() > 0.8) {
      throw new Error('Insufficient funds');
    }
  }
  
  @authenticated
  @memoize((accountId: string) => accountId, 30000)
  @monitor()
  getAccountBalance(accountId: string): number {
    console.log(`💰 Fetching balance for ${accountId}`);
    return Math.random() * 10000;
  }
}

// 💫 Testing combined decorators
const banking = new BankingService();

// Test transfer with all decorators
banking.transferMoney('ACC001', 'ACC002', 1000)
  .then(() => console.log('Transfer successful'))
  .catch(e => console.error('Transfer failed:', e.message));

// Test cached balance
console.log('Balance:', banking.getAccountBalance('ACC001'));
console.log('Balance (cached):', banking.getAccountBalance('ACC001'));

🎮 Hands-On Exercise

Let’s build a form validation system using decorators!

📝 Challenge: Form Field Validation System

Create a validation system that:

  1. Validates form fields using decorators
  2. Supports custom validators
  3. Provides detailed error messages
  4. Handles async validation
// Your challenge: Implement this form validation system
interface FieldOptions {
  label?: string;
  placeholder?: string;
  defaultValue?: any;
}

// Decorators to implement:
// @Field(options) - Mark a property as a form field
// @Required - Field must have a value
// @Email - Must be valid email
// @MinLength(n) - Minimum string length
// @MaxLength(n) - Maximum string length
// @Pattern(regex) - Must match pattern
// @AsyncValidator(fn) - Custom async validation

// Example usage to support:
class RegistrationForm {
  @Field({ label: 'Username' })
  @Required
  @MinLength(3)
  @MaxLength(20)
  @Pattern(/^[a-zA-Z0-9_]+$/)
  username!: string;
  
  @Field({ label: 'Email Address' })
  @Required
  @Email
  @AsyncValidator(async (email: string) => {
    // Check if email is already taken
    const taken = ['[email protected]', '[email protected]'];
    return !taken.includes(email);
  })
  email!: string;
  
  @Field({ label: 'Password' })
  @Required
  @MinLength(8)
  @Pattern(/^(?=.*[A-Za-z])(?=.*\d)/)
  password!: string;
}

// Implement the validation system!

💡 Solution

Click to see the solution
import 'reflect-metadata';

// 🎯 Metadata keys
const FIELD_KEY = Symbol('field');
const VALIDATORS_KEY = Symbol('validators');
const ASYNC_VALIDATORS_KEY = Symbol('asyncValidators');

// 📊 Validation interfaces
interface FieldMetadata {
  label?: string;
  placeholder?: string;
  defaultValue?: any;
  propertyKey: string;
}

interface ValidationError {
  field: string;
  message: string;
}

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

interface Validator {
  validate: (value: any) => boolean;
  message: (fieldName: string, value: any) => string;
}

interface AsyncValidator {
  validate: (value: any) => Promise<boolean>;
  message: (fieldName: string, value: any) => string;
}

// 🏗️ Base form class
class FormBase {
  private _values = new Map<string, any>();
  private _errors = new Map<string, string[]>();
  private _touched = new Set<string>();
  
  constructor() {
    this.initializeFields();
  }
  
  private initializeFields() {
    const fields = Reflect.getMetadata(FIELD_KEY, this) || new Map();
    
    fields.forEach((metadata: FieldMetadata, propertyKey: string) => {
      // Set default values
      if (metadata.defaultValue !== undefined) {
        this._values.set(propertyKey, metadata.defaultValue);
      }
      
      // Create property accessors
      Object.defineProperty(this, propertyKey, {
        get: () => this._values.get(propertyKey),
        set: (value) => {
          this._values.set(propertyKey, value);
          this._touched.add(propertyKey);
          this.validateField(propertyKey);
        },
        enumerable: true,
        configurable: true
      });
    });
  }
  
  private async validateField(propertyKey: string): Promise<boolean> {
    const value = this._values.get(propertyKey);
    const errors: string[] = [];
    
    // Get field metadata
    const fields = Reflect.getMetadata(FIELD_KEY, this) || new Map();
    const fieldMeta = fields.get(propertyKey);
    const fieldName = fieldMeta?.label || propertyKey;
    
    // Sync validators
    const validators = Reflect.getMetadata(VALIDATORS_KEY, this, propertyKey) || [];
    for (const validator of validators) {
      if (!validator.validate(value)) {
        errors.push(validator.message(fieldName, value));
      }
    }
    
    // Async validators
    const asyncValidators = Reflect.getMetadata(ASYNC_VALIDATORS_KEY, this, propertyKey) || [];
    for (const validator of asyncValidators) {
      const valid = await validator.validate(value);
      if (!valid) {
        errors.push(validator.message(fieldName, value));
      }
    }
    
    this._errors.set(propertyKey, errors);
    return errors.length === 0;
  }
  
  async validate(): Promise<ValidationResult> {
    const errors: ValidationError[] = [];
    const fields = Reflect.getMetadata(FIELD_KEY, this) || new Map();
    
    // Validate all fields
    for (const [propertyKey, metadata] of fields) {
      await this.validateField(propertyKey);
      
      const fieldErrors = this._errors.get(propertyKey) || [];
      fieldErrors.forEach(message => {
        errors.push({
          field: propertyKey,
          message
        });
      });
    }
    
    return {
      valid: errors.length === 0,
      errors
    };
  }
  
  getErrors(): Record<string, string[]> {
    const result: Record<string, string[]> = {};
    this._errors.forEach((errors, field) => {
      if (errors.length > 0) {
        result[field] = errors;
      }
    });
    return result;
  }
  
  getValues(): Record<string, any> {
    return Object.fromEntries(this._values);
  }
  
  reset(): void {
    const fields = Reflect.getMetadata(FIELD_KEY, this) || new Map();
    
    this._values.clear();
    this._errors.clear();
    this._touched.clear();
    
    // Reset to default values
    fields.forEach((metadata: FieldMetadata) => {
      if (metadata.defaultValue !== undefined) {
        this._values.set(metadata.propertyKey, metadata.defaultValue);
      }
    });
  }
}

// 🎯 Field decorator
function Field(options: FieldOptions = {}) {
  return function(target: any, propertyKey: string) {
    const fields = Reflect.getMetadata(FIELD_KEY, target) || new Map();
    
    fields.set(propertyKey, {
      ...options,
      propertyKey
    });
    
    Reflect.defineMetadata(FIELD_KEY, fields, target);
  };
}

// ✅ Required validator
function Required(target: any, propertyKey: string) {
  const validators = Reflect.getMetadata(VALIDATORS_KEY, target, propertyKey) || [];
  
  validators.push({
    validate: (value: any) => value !== undefined && value !== null && value !== '',
    message: (fieldName: string) => `${fieldName} is required`
  });
  
  Reflect.defineMetadata(VALIDATORS_KEY, validators, target, propertyKey);
}

// 📧 Email validator
function Email(target: any, propertyKey: string) {
  const validators = Reflect.getMetadata(VALIDATORS_KEY, target, propertyKey) || [];
  
  validators.push({
    validate: (value: any) => {
      if (!value) return true; // Let Required handle empty values
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      return emailRegex.test(value);
    },
    message: (fieldName: string) => `${fieldName} must be a valid email address`
  });
  
  Reflect.defineMetadata(VALIDATORS_KEY, validators, target, propertyKey);
}

// 📏 Length validators
function MinLength(min: number) {
  return function(target: any, propertyKey: string) {
    const validators = Reflect.getMetadata(VALIDATORS_KEY, target, propertyKey) || [];
    
    validators.push({
      validate: (value: any) => !value || value.length >= min,
      message: (fieldName: string, value: any) => 
        `${fieldName} must be at least ${min} characters (current: ${value?.length || 0})`
    });
    
    Reflect.defineMetadata(VALIDATORS_KEY, validators, target, propertyKey);
  };
}

function MaxLength(max: number) {
  return function(target: any, propertyKey: string) {
    const validators = Reflect.getMetadata(VALIDATORS_KEY, target, propertyKey) || [];
    
    validators.push({
      validate: (value: any) => !value || value.length <= max,
      message: (fieldName: string, value: any) => 
        `${fieldName} must be at most ${max} characters (current: ${value?.length || 0})`
    });
    
    Reflect.defineMetadata(VALIDATORS_KEY, validators, target, propertyKey);
  };
}

// 🎯 Pattern validator
function Pattern(pattern: RegExp, message?: string) {
  return function(target: any, propertyKey: string) {
    const validators = Reflect.getMetadata(VALIDATORS_KEY, target, propertyKey) || [];
    
    validators.push({
      validate: (value: any) => !value || pattern.test(value),
      message: (fieldName: string) => 
        message || `${fieldName} must match pattern ${pattern}`
    });
    
    Reflect.defineMetadata(VALIDATORS_KEY, validators, target, propertyKey);
  };
}

// 🔄 Async validator
function AsyncValidator(
  validator: (value: any) => Promise<boolean>,
  message?: string
) {
  return function(target: any, propertyKey: string) {
    const validators = Reflect.getMetadata(ASYNC_VALIDATORS_KEY, target, propertyKey) || [];
    
    validators.push({
      validate: validator,
      message: (fieldName: string) => 
        message || `${fieldName} failed validation`
    });
    
    Reflect.defineMetadata(ASYNC_VALIDATORS_KEY, validators, target, propertyKey);
  };
}

// 🏠 Example form implementation
class RegistrationForm extends FormBase {
  @Field({ label: 'Username', placeholder: 'Enter username' })
  @Required
  @MinLength(3)
  @MaxLength(20)
  @Pattern(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores')
  username!: string;
  
  @Field({ label: 'Email Address', placeholder: '[email protected]' })
  @Required
  @Email
  @AsyncValidator(
    async (email: string) => {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 300));
      const taken = ['[email protected]', '[email protected]'];
      return !taken.includes(email);
    },
    'This email is already registered'
  )
  email!: string;
  
  @Field({ label: 'Password', placeholder: 'Min 8 characters' })
  @Required
  @MinLength(8)
  @Pattern(
    /^(?=.*[A-Za-z])(?=.*\d)/,
    'Password must contain at least one letter and one number'
  )
  password!: string;
  
  @Field({ label: 'Confirm Password' })
  @Required
  @AsyncValidator(
    async function(this: RegistrationForm, value: string) {
      return value === this.password;
    },
    'Passwords do not match'
  )
  confirmPassword!: string;
  
  @Field({ label: 'Terms & Conditions', defaultValue: false })
  @AsyncValidator(
    async (value: boolean) => value === true,
    'You must accept the terms and conditions'
  )
  termsAccepted!: boolean;
}

// 💫 Testing the form system
async function testFormValidation() {
  console.log('=== Form Validation Demo ===\n');
  
  const form = new RegistrationForm();
  
  // Test invalid form
  form.username = 'ab'; // Too short
  form.email = 'invalid-email';
  form.password = 'weak';
  form.confirmPassword = 'different';
  form.termsAccepted = false;
  
  console.log('Testing invalid form...');
  const result1 = await form.validate();
  console.log('Valid:', result1.valid);
  console.log('Errors:', form.getErrors());
  
  // Test valid form
  console.log('\nTesting valid form...');
  form.username = 'john_doe';
  form.email = '[email protected]';
  form.password = 'SecurePass123';
  form.confirmPassword = 'SecurePass123';
  form.termsAccepted = true;
  
  const result2 = await form.validate();
  console.log('Valid:', result2.valid);
  console.log('Values:', form.getValues());
  
  // Test async validation
  console.log('\nTesting taken email...');
  form.email = '[email protected]';
  const result3 = await form.validate();
  console.log('Valid:', result3.valid);
  console.log('Email errors:', form.getErrors().email);
}

testFormValidation();

🎯 Summary

You’ve mastered method and property decorators in TypeScript! 🎉 You learned how to:

  • 🎯 Create powerful method decorators for cross-cutting concerns
  • 🏗️ Implement property decorators with custom getters/setters
  • 📊 Build validation and transformation decorators
  • 🔧 Use parameter decorators for metadata and DI
  • 🎨 Combine multiple decorators for complex behaviors
  • ✨ Create real-world patterns like caching, monitoring, and validation

Method and property decorators provide fine-grained control over your class members, enabling you to add sophisticated behavior exactly where needed!

Keep exploring the precision world of TypeScript decorators! 🚀