+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 53 of 354

🏗 ️ Class Decorators: Enhancing Classes Dynamically

Master class decorators in TypeScript to modify, extend, and enhance classes with powerful runtime transformations 🚀

💎Advanced
30 min read

Prerequisites

  • Understanding of decorators basics 📝
  • Knowledge of class prototypes 🔍
  • Familiarity with constructor functions 💻

What you'll learn

  • Create powerful class decorators 🎯
  • Modify class constructors and prototypes 🏗️
  • Implement mixins via decorators 🛡️
  • Build real-world decorator patterns ✨

🎯 Introduction

Welcome to the architectural world of class decorators! 🎉 In this guide, we’ll explore how to use decorators to transform entire classes, adding features, modifying behavior, and creating powerful abstractions at the class level.

You’ll discover how class decorators are like master builders 🏗️ - they can renovate, extend, or completely transform your classes! Whether you’re implementing singleton patterns 🎯, adding logging capabilities 📊, or building ORM mappings 💾, class decorators provide elegant solutions.

By the end of this tutorial, you’ll be confidently creating decorators that enhance your classes with superpowers! Let’s start building! 🏊‍♂️

📚 Understanding Class Decorators

🤔 How Class Decorators Work

Class decorators are functions that receive the constructor function as their only parameter. They can modify the constructor, its prototype, or even replace the entire class with a new implementation.

Think of class decorators like:

  • 🏗️ Architects: Redesigning the blueprint of your building
  • 🎨 Painters: Adding new colors and features to existing structures
  • 🔧 Mechanics: Installing new engines in your vehicles
  • 🧬 Genetic engineers: Modifying DNA to add new traits

💡 Class Decorator Signature

type ClassDecorator = <TFunction extends Function>(
  target: TFunction
) => TFunction | void;

The decorator can either:

  1. Modify the class in place (return void)
  2. Return a new class to replace the original

🔧 Basic Class Decorators

📝 Simple Enhancement Patterns

Let’s start with fundamental class decorator patterns:

// 🎯 Sealed class decorator
function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
  console.log(`🔒 Sealed class: ${constructor.name}`);
}

// 🏷️ Adding metadata to classes
function metadata<T extends { new(...args: any[]): {} }>(data: any) {
  return function(constructor: T) {
    // Add metadata to the constructor
    (constructor as any).metadata = data;
    
    // Add instance method to access metadata
    constructor.prototype.getMetadata = function() {
      return (constructor as any).metadata;
    };
    
    console.log(`📊 Added metadata to ${constructor.name}:`, data);
  };
}

// 📝 Logger decorator
function withLogging<T extends { new(...args: any[]): {} }>(constructor: T) {
  return class extends constructor {
    constructor(...args: any[]) {
      console.log(`🔨 Creating instance of ${constructor.name} with args:`, args);
      super(...args);
      console.log(`✅ Instance of ${constructor.name} created`);
    }
  };
}

// 🕐 Timestamp decorator
function timestamped<T extends { new(...args: any[]): {} }>(constructor: T) {
  return class extends constructor {
    createdAt = new Date();
    updatedAt = new Date();
    
    update() {
      this.updatedAt = new Date();
      if (super.update) {
        super.update();
      }
    }
    
    getAge(): number {
      return Date.now() - this.createdAt.getTime();
    }
  };
}

// 🏠 Using decorators
@sealed
@metadata({ version: '1.0.0', author: 'John Doe' })
@withLogging
@timestamped
class Product {
  constructor(
    public id: string,
    public name: string,
    public price: number
  ) {}
  
  update() {
    console.log('Product updated');
  }
}

// 💫 Testing
const product = new Product('123', 'Laptop', 999);
console.log('Product age:', product.getAge(), 'ms');
console.log('Metadata:', product.getMetadata());

// Try to extend sealed class (will fail in strict mode)
// class ExtendedProduct extends Product {} // Error!

🏗️ Constructor Modification

Modifying class constructors dynamically:

// 🎯 Auto-ID decorator
let nextId = 1;

function autoId<T extends { new(...args: any[]): {} }>(constructor: T) {
  return class extends constructor {
    id: number;
    
    constructor(...args: any[]) {
      super(...args);
      this.id = nextId++;
    }
  };
}

// 🔐 Singleton decorator
function singleton<T extends { new(...args: any[]): {} }>(constructor: T) {
  let instance: T;
  
  // Return a new constructor function
  const newConstructor: any = function(...args: any[]) {
    if (instance) {
      console.log('🔄 Returning existing instance');
      return instance;
    }
    
    console.log('🆕 Creating new instance');
    instance = new constructor(...args);
    return instance;
  };
  
  // Copy prototype properties
  newConstructor.prototype = constructor.prototype;
  
  // Copy static properties
  Object.setPrototypeOf(newConstructor, constructor);
  
  return newConstructor;
}

// 🏁 Initialization decorator
interface Initializable {
  init?(): void | Promise<void>;
}

function autoInit<T extends { new(...args: any[]): Initializable }>(constructor: T) {
  return class extends constructor {
    constructor(...args: any[]) {
      super(...args);
      
      // Auto-call init if it exists
      if (this.init) {
        const result = this.init();
        if (result instanceof Promise) {
          result.catch(err => {
            console.error('❌ Initialization failed:', err);
          });
        }
      }
    }
  };
}

// 🏠 Example usage
@autoId
class User {
  name: string;
  
  constructor(name: string) {
    this.name = name;
  }
}

@singleton
@autoInit
class Database implements Initializable {
  private connected = false;
  
  async init() {
    console.log('🔌 Initializing database connection...');
    await new Promise(resolve => setTimeout(resolve, 100));
    this.connected = true;
    console.log('✅ Database connected');
  }
  
  isConnected() {
    return this.connected;
  }
}

// 💫 Testing
const user1 = new User('Alice');
const user2 = new User('Bob');
console.log('User IDs:', user1.id, user2.id); // Different IDs

const db1 = new Database();
const db2 = new Database();
console.log('Same instance?', db1 === db2); // true

🚀 Advanced Class Decorator Patterns

🎭 Mixin Implementation via Decorators

Creating powerful mixin patterns with decorators:

// 🎯 Mixin type helpers
type Constructor<T = {}> = new (...args: any[]) => T;
type Mixin<T extends Constructor> = T;

// 🏃‍♂️ Movement mixin decorator
interface Movable {
  x: number;
  y: number;
  move(dx: number, dy: number): void;
}

function withMovement<T extends Constructor>(Base: T): Mixin<T & Constructor<Movable>> {
  return class extends Base implements Movable {
    x = 0;
    y = 0;
    
    move(dx: number, dy: number) {
      this.x += dx;
      this.y += dy;
      console.log(`📍 Moved to (${this.x}, ${this.y})`);
    }
  };
}

// 🎨 Observable mixin decorator
interface Observable {
  observers: Map<string, Set<Function>>;
  subscribe(event: string, callback: Function): () => void;
  notify(event: string, data?: any): void;
}

function withObservable<T extends Constructor>(Base: T): Mixin<T & Constructor<Observable>> {
  return class extends Base implements Observable {
    observers = new Map<string, Set<Function>>();
    
    subscribe(event: string, callback: Function): () => void {
      if (!this.observers.has(event)) {
        this.observers.set(event, new Set());
      }
      
      this.observers.get(event)!.add(callback);
      
      // Return unsubscribe function
      return () => {
        this.observers.get(event)?.delete(callback);
      };
    }
    
    notify(event: string, data?: any) {
      this.observers.get(event)?.forEach(callback => {
        callback(data);
      });
    }
  };
}

// 💾 Serializable mixin decorator
interface Serializable {
  serialize(): string;
  deserialize(data: string): void;
}

function withSerialization<T extends Constructor>(Base: T): Mixin<T & Constructor<Serializable>> {
  return class extends Base implements Serializable {
    serialize(): string {
      const data: any = {};
      
      // Get all properties
      Object.keys(this).forEach(key => {
        const value = (this as any)[key];
        
        // Skip functions and complex objects
        if (typeof value !== 'function' && !(value instanceof Map) && !(value instanceof Set)) {
          data[key] = value;
        }
      });
      
      return JSON.stringify(data);
    }
    
    deserialize(json: string) {
      const data = JSON.parse(json);
      Object.assign(this, data);
    }
  };
}

// 🏠 Composed class with multiple mixins
@withSerialization
@withObservable
@withMovement
class GameObject {
  constructor(public name: string) {}
  
  destroy() {
    this.notify('destroy', { name: this.name });
    console.log(`💥 ${this.name} destroyed`);
  }
}

// 💫 Usage
const player = new GameObject('Player');

// Observable functionality
const unsubscribe = player.subscribe('destroy', (data) => {
  console.log('🔔 Object destroyed:', data);
});

// Movement functionality
player.move(10, 20);

// Serialization functionality
const saved = player.serialize();
console.log('💾 Saved state:', saved);

player.move(5, 5);
const newPlayer = new GameObject('NewPlayer');
newPlayer.deserialize(saved);
console.log('📂 Restored position:', newPlayer.x, newPlayer.y);

player.destroy();

🏗️ Validation and Schema Decorators

Building validation into classes:

// 🎯 Schema definition
interface PropertySchema {
  type: 'string' | 'number' | 'boolean' | 'date';
  required?: boolean;
  min?: number;
  max?: number;
  pattern?: RegExp;
  validator?: (value: any) => boolean;
}

interface ClassSchema {
  [property: string]: PropertySchema;
}

// 📊 Schema decorator
function schema(classSchema: ClassSchema) {
  return function<T extends { new(...args: any[]): {} }>(constructor: T) {
    // Store schema on constructor
    (constructor as any).__schema = classSchema;
    
    // Add validation method
    constructor.prototype.validate = function(): { valid: boolean; errors: string[] } {
      const errors: string[] = [];
      const schema = (constructor as any).__schema;
      
      for (const [prop, propSchema] of Object.entries(schema)) {
        const value = (this as any)[prop];
        
        // Required check
        if (propSchema.required && (value === undefined || value === null)) {
          errors.push(`${prop} is required`);
          continue;
        }
        
        if (value === undefined || value === null) continue;
        
        // Type check
        const actualType = value instanceof Date ? 'date' : typeof value;
        if (actualType !== propSchema.type) {
          errors.push(`${prop} must be of type ${propSchema.type}`);
          continue;
        }
        
        // Min/max checks
        if (propSchema.min !== undefined && value < propSchema.min) {
          errors.push(`${prop} must be at least ${propSchema.min}`);
        }
        
        if (propSchema.max !== undefined && value > propSchema.max) {
          errors.push(`${prop} must be at most ${propSchema.max}`);
        }
        
        // Pattern check
        if (propSchema.pattern && !propSchema.pattern.test(value)) {
          errors.push(`${prop} does not match required pattern`);
        }
        
        // Custom validator
        if (propSchema.validator && !propSchema.validator(value)) {
          errors.push(`${prop} failed custom validation`);
        }
      }
      
      return { valid: errors.length === 0, errors };
    };
    
    // Add sanitization method
    constructor.prototype.sanitize = function() {
      const schema = (constructor as any).__schema;
      
      for (const [prop, propSchema] of Object.entries(schema)) {
        const value = (this as any)[prop];
        
        if (value === undefined || value === null) continue;
        
        // Type coercion
        switch (propSchema.type) {
          case 'string':
            (this as any)[prop] = String(value).trim();
            break;
          case 'number':
            (this as any)[prop] = Number(value);
            break;
          case 'boolean':
            (this as any)[prop] = Boolean(value);
            break;
          case 'date':
            if (!(value instanceof Date)) {
              (this as any)[prop] = new Date(value);
            }
            break;
        }
      }
    };
    
    return constructor;
  };
}

// 🔒 Immutable decorator
function immutable<T extends { new(...args: any[]): {} }>(constructor: T) {
  return class extends constructor {
    constructor(...args: any[]) {
      super(...args);
      Object.freeze(this);
    }
  };
}

// 🏠 Example with validation
@schema({
  username: {
    type: 'string',
    required: true,
    min: 3,
    max: 20,
    pattern: /^[a-zA-Z0-9_]+$/
  },
  email: {
    type: 'string',
    required: true,
    pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  },
  age: {
    type: 'number',
    min: 0,
    max: 150
  },
  active: {
    type: 'boolean',
    required: true
  }
})
class UserAccount {
  username?: string;
  email?: string;
  age?: number;
  active?: boolean;
  
  constructor(data: Partial<UserAccount>) {
    Object.assign(this, data);
  }
}

// 💫 Testing validation
const account = new UserAccount({
  username: 'john_doe',
  email: '[email protected]',
  age: 25,
  active: true
});

const validation = account.validate();
console.log('Valid:', validation.valid);
if (!validation.valid) {
  console.log('Errors:', validation.errors);
}

// Test with invalid data
const invalidAccount = new UserAccount({
  username: 'ab', // Too short
  email: 'invalid-email',
  age: 200, // Too old
  active: true
});

const invalidValidation = invalidAccount.validate();
console.log('\nInvalid account errors:', invalidValidation.errors);

🎪 Real-World Patterns

🌐 ORM-Style Decorators

Creating database entity decorators:

// 🎯 Entity metadata
interface EntityMetadata {
  tableName: string;
  columns: Map<string, ColumnMetadata>;
  relations: Map<string, RelationMetadata>;
}

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

interface RelationMetadata {
  type: 'one-to-one' | 'one-to-many' | 'many-to-many';
  target: string;
  joinColumn?: string;
}

// 📊 Metadata storage
const entityMetadata = new Map<Function, EntityMetadata>();

// 🏗️ Entity decorator
function Entity(tableName: string) {
  return function<T extends { new(...args: any[]): {} }>(constructor: T) {
    // Initialize metadata
    const metadata: EntityMetadata = {
      tableName,
      columns: new Map(),
      relations: new Map()
    };
    
    entityMetadata.set(constructor, metadata);
    
    // Add repository methods
    constructor.prototype.save = async function() {
      console.log(`💾 Saving ${tableName} entity:`, this);
      // Simulate save operation
      return this;
    };
    
    constructor.prototype.delete = async function() {
      console.log(`🗑️ Deleting ${tableName} entity:`, this);
      // Simulate delete operation
    };
    
    // Add static repository methods
    (constructor as any).find = async function(conditions: any) {
      console.log(`🔍 Finding ${tableName} with conditions:`, conditions);
      // Simulate find operation
      return [];
    };
    
    (constructor as any).findOne = async function(conditions: any) {
      console.log(`🔍 Finding one ${tableName} with conditions:`, conditions);
      // Simulate findOne operation
      return new constructor();
    };
    
    (constructor as any).create = function(data: any) {
      console.log(`🆕 Creating ${tableName} entity`);
      return new constructor(data);
    };
    
    return constructor;
  };
}

// 🔧 Column decorator factory
function Column(options: Partial<ColumnMetadata> = {}) {
  return function(target: any, propertyKey: string) {
    const constructor = target.constructor;
    const metadata = entityMetadata.get(constructor);
    
    if (metadata) {
      metadata.columns.set(propertyKey, {
        name: options.name || propertyKey,
        type: options.type || 'string',
        ...options
      });
    }
  };
}

// 🔑 Primary key decorator
function PrimaryKey(target: any, propertyKey: string) {
  Column({ primaryKey: true })(target, propertyKey);
}

// 🌟 Relation decorators
function OneToMany(targetEntity: string, joinColumn?: string) {
  return function(target: any, propertyKey: string) {
    const constructor = target.constructor;
    const metadata = entityMetadata.get(constructor);
    
    if (metadata) {
      metadata.relations.set(propertyKey, {
        type: 'one-to-many',
        target: targetEntity,
        joinColumn
      });
    }
  };
}

function ManyToOne(targetEntity: string, joinColumn?: string) {
  return function(target: any, propertyKey: string) {
    const constructor = target.constructor;
    const metadata = entityMetadata.get(constructor);
    
    if (metadata) {
      metadata.relations.set(propertyKey, {
        type: 'one-to-many',
        target: targetEntity,
        joinColumn
      });
    }
  };
}

// 🏠 Example entities
@Entity('users')
class User {
  @PrimaryKey
  @Column({ type: 'uuid' })
  id!: string;
  
  @Column({ unique: true })
  username!: string;
  
  @Column({ unique: true })
  email!: string;
  
  @Column({ type: 'date', default: () => new Date() })
  createdAt!: Date;
  
  @OneToMany('Post', 'userId')
  posts!: Post[];
  
  constructor(data?: Partial<User>) {
    if (data) Object.assign(this, data);
  }
}

@Entity('posts')
class Post {
  @PrimaryKey
  @Column({ type: 'uuid' })
  id!: string;
  
  @Column()
  title!: string;
  
  @Column({ type: 'text' })
  content!: string;
  
  @Column({ type: 'uuid' })
  userId!: string;
  
  @ManyToOne('User', 'userId')
  user!: User;
  
  constructor(data?: Partial<Post>) {
    if (data) Object.assign(this, data);
  }
}

// 💫 Usage
const user = User.create({
  id: '123',
  username: 'johndoe',
  email: '[email protected]'
});

await user.save();

const posts = await Post.find({ userId: '123' });
console.log('Found posts:', posts);

// 🔍 Inspect metadata
function inspectEntity(EntityClass: Function) {
  const metadata = entityMetadata.get(EntityClass);
  if (!metadata) return;
  
  console.log(`\n📊 Entity: ${EntityClass.name} -> ${metadata.tableName}`);
  console.log('Columns:');
  metadata.columns.forEach((col, prop) => {
    console.log(`  - ${prop}: ${col.type}${col.primaryKey ? ' [PK]' : ''}${col.unique ? ' [UNIQUE]' : ''}`);
  });
  
  if (metadata.relations.size > 0) {
    console.log('Relations:');
    metadata.relations.forEach((rel, prop) => {
      console.log(`  - ${prop}: ${rel.type} -> ${rel.target}`);
    });
  }
}

inspectEntity(User);
inspectEntity(Post);

🎮 Component System Decorators

Building a game component system:

// 🎯 Component registry
const componentRegistry = new Map<string, Function>();
const componentMetadata = new Map<Function, ComponentMetadata>();

interface ComponentMetadata {
  name: string;
  updatePriority: number;
  dependencies: string[];
}

// 🏗️ Component decorator
function Component(options: Partial<ComponentMetadata> = {}) {
  return function<T extends { new(...args: any[]): {} }>(constructor: T) {
    const name = options.name || constructor.name;
    const metadata: ComponentMetadata = {
      name,
      updatePriority: options.updatePriority || 0,
      dependencies: options.dependencies || []
    };
    
    componentRegistry.set(name, constructor);
    componentMetadata.set(constructor, metadata);
    
    // Add component lifecycle methods
    if (!constructor.prototype.onAttach) {
      constructor.prototype.onAttach = function() {
        console.log(`🔗 ${name} attached`);
      };
    }
    
    if (!constructor.prototype.onDetach) {
      constructor.prototype.onDetach = function() {
        console.log(`🔓 ${name} detached`);
      };
    }
    
    if (!constructor.prototype.update) {
      constructor.prototype.update = function(deltaTime: number) {
        // Default empty update
      };
    }
    
    return constructor;
  };
}

// 🎨 Auto-wire dependencies
function Inject(componentName: string) {
  return function(target: any, propertyKey: string) {
    Object.defineProperty(target, propertyKey, {
      get() {
        const entity = (this as any).entity;
        if (!entity) return null;
        
        return entity.getComponent(componentName);
      },
      enumerable: true,
      configurable: true
    });
  };
}

// 🏠 GameObject with component system
@Component({ name: 'GameObject' })
class GameObject {
  private components = new Map<string, any>();
  public name: string;
  
  constructor(name: string) {
    this.name = name;
  }
  
  addComponent(ComponentClass: Function): this {
    const metadata = componentMetadata.get(ComponentClass);
    if (!metadata) {
      throw new Error('Not a valid component class');
    }
    
    // Check dependencies
    for (const dep of metadata.dependencies) {
      if (!this.components.has(dep)) {
        throw new Error(`Missing dependency: ${dep}`);
      }
    }
    
    const component = new (ComponentClass as any)();
    component.entity = this;
    
    this.components.set(metadata.name, component);
    component.onAttach();
    
    return this;
  }
  
  getComponent<T>(name: string): T | null {
    return this.components.get(name) || null;
  }
  
  removeComponent(name: string): boolean {
    const component = this.components.get(name);
    if (component) {
      component.onDetach();
      return this.components.delete(name);
    }
    return false;
  }
  
  update(deltaTime: number): void {
    // Sort components by priority
    const sorted = Array.from(this.components.entries()).sort((a, b) => {
      const aData = componentMetadata.get(a[1].constructor)!;
      const bData = componentMetadata.get(b[1].constructor)!;
      return bData.updatePriority - aData.updatePriority;
    });
    
    // Update in priority order
    for (const [name, component] of sorted) {
      component.update(deltaTime);
    }
  }
}

// 🎮 Example components
@Component({ updatePriority: 10 })
class Transform {
  x = 0;
  y = 0;
  rotation = 0;
  
  move(dx: number, dy: number): void {
    this.x += dx;
    this.y += dy;
  }
  
  update(deltaTime: number): void {
    // Transform updates first (high priority)
  }
}

@Component({ dependencies: ['Transform'] })
class Physics {
  @Inject('Transform')
  transform!: Transform;
  
  velocityX = 0;
  velocityY = 0;
  gravity = 9.8;
  
  update(deltaTime: number): void {
    this.velocityY += this.gravity * deltaTime;
    this.transform.move(
      this.velocityX * deltaTime,
      this.velocityY * deltaTime
    );
  }
}

@Component({ dependencies: ['Transform'] })
class Renderer {
  @Inject('Transform')
  transform!: Transform;
  
  color = '#ffffff';
  visible = true;
  
  update(deltaTime: number): void {
    if (this.visible) {
      console.log(`🎨 Rendering at (${this.transform.x.toFixed(2)}, ${this.transform.y.toFixed(2)})`);
    }
  }
}

// 💫 Usage
const player = new GameObject('Player');
player
  .addComponent(Transform)
  .addComponent(Physics)
  .addComponent(Renderer);

const transform = player.getComponent<Transform>('Transform')!;
transform.move(10, 0);

const physics = player.getComponent<Physics>('Physics')!;
physics.velocityX = 5;

// Simulate game loop
for (let i = 0; i < 3; i++) {
  console.log(`\n⏱️ Frame ${i + 1}:`);
  player.update(0.016); // 60 FPS
}

🎮 Hands-On Exercise

Let’s build a caching system using class decorators!

📝 Challenge: Smart Caching System

Create a caching system that:

  1. Automatically caches method results
  2. Supports TTL (time to live)
  3. Provides cache statistics
  4. Allows cache invalidation
// Your challenge: Implement this caching system
interface CacheOptions {
  ttl?: number; // Time to live in milliseconds
  maxSize?: number; // Maximum cache entries
  key?: (...args: any[]) => string; // Custom key generator
}

// Decorators to implement:
// @Cacheable(options) - Class decorator that adds caching
// @CacheKey - Parameter decorator to mark cache key params
// @CacheClear(method) - Method decorator to clear cache
// @CacheStats - Property decorator to expose cache stats

// Example usage to support:
@Cacheable({ ttl: 5000, maxSize: 100 })
class WeatherService {
  @CacheStats
  stats: any;
  
  async getWeather(@CacheKey city: string, details: boolean = false): Promise<any> {
    console.log(`Fetching weather for ${city}...`);
    // Simulate API call
    await new Promise(resolve => setTimeout(resolve, 1000));
    return { city, temp: Math.random() * 30, details };
  }
  
  @CacheClear('getWeather')
  async updateWeather(city: string, data: any): Promise<void> {
    console.log(`Updating weather for ${city}`);
    // This should clear cache for the city
  }
}

// Implement the decorators!

💡 Solution

Click to see the solution
// 🎯 Cache entry interface
interface CacheEntry {
  value: any;
  timestamp: number;
  hits: number;
}

// 📊 Cache statistics
interface CacheStatistics {
  hits: number;
  misses: number;
  entries: number;
  hitRate: number;
  evictions: number;
}

// 🔑 Cache key metadata
const CACHE_KEY_METADATA = Symbol('cacheKey');

// 🏗️ Main cacheable decorator
function Cacheable(options: CacheOptions = {}) {
  const { ttl = Infinity, maxSize = 100 } = options;
  
  return function<T extends { new(...args: any[]): {} }>(constructor: T) {
    // Create cache storage
    const cache = new Map<string, CacheEntry>();
    const stats: CacheStatistics = {
      hits: 0,
      misses: 0,
      entries: 0,
      hitRate: 0,
      evictions: 0
    };
    
    // Get original methods
    const prototype = constructor.prototype;
    const propertyNames = Object.getOwnPropertyNames(prototype);
    
    // Cache management functions
    const generateKey = (methodName: string, args: any[], keyParams?: number[]): string => {
      if (options.key) {
        return options.key(...args);
      }
      
      // Use only marked parameters for key
      const keyArgs = keyParams 
        ? args.filter((_, index) => keyParams.includes(index))
        : args;
      
      return `${methodName}:${JSON.stringify(keyArgs)}`;
    };
    
    const evictOldest = () => {
      let oldestKey: string | null = null;
      let oldestTime = Infinity;
      
      cache.forEach((entry, key) => {
        if (entry.timestamp < oldestTime) {
          oldestTime = entry.timestamp;
          oldestKey = key;
        }
      });
      
      if (oldestKey) {
        cache.delete(oldestKey);
        stats.evictions++;
        stats.entries--;
      }
    };
    
    const updateHitRate = () => {
      const total = stats.hits + stats.misses;
      stats.hitRate = total > 0 ? stats.hits / total : 0;
    };
    
    // Wrap cacheable methods
    propertyNames.forEach(propertyName => {
      const descriptor = Object.getOwnPropertyDescriptor(prototype, propertyName);
      if (!descriptor || typeof descriptor.value !== 'function' || propertyName === 'constructor') {
        return;
      }
      
      const originalMethod = descriptor.value;
      
      // Check for cache key parameters
      const cacheKeyParams = Reflect.getMetadata(CACHE_KEY_METADATA, prototype, propertyName);
      
      descriptor.value = async function(...args: any[]) {
        const key = generateKey(propertyName, args, cacheKeyParams);
        
        // Check cache
        const cached = cache.get(key);
        if (cached && Date.now() - cached.timestamp < ttl) {
          cached.hits++;
          stats.hits++;
          updateHitRate();
          console.log(`💾 Cache hit for ${propertyName}`);
          return cached.value;
        }
        
        // Cache miss
        stats.misses++;
        updateHitRate();
        console.log(`🔄 Cache miss for ${propertyName}`);
        
        // Call original method
        const result = await originalMethod.apply(this, args);
        
        // Store in cache
        if (cache.size >= maxSize) {
          evictOldest();
        }
        
        cache.set(key, {
          value: result,
          timestamp: Date.now(),
          hits: 0
        });
        stats.entries = cache.size;
        
        return result;
      };
      
      Object.defineProperty(prototype, propertyName, descriptor);
    });
    
    // Add cache management methods
    prototype._clearCache = function(methodName?: string) {
      if (methodName) {
        // Clear specific method cache
        const keysToDelete: string[] = [];
        cache.forEach((_, key) => {
          if (key.startsWith(`${methodName}:`)) {
            keysToDelete.push(key);
          }
        });
        keysToDelete.forEach(key => cache.delete(key));
        stats.entries = cache.size;
        console.log(`🗑️ Cleared cache for ${methodName}`);
      } else {
        // Clear all cache
        cache.clear();
        stats.entries = 0;
        console.log('🗑️ Cleared all cache');
      }
    };
    
    prototype._getCacheStats = function(): CacheStatistics {
      return { ...stats };
    };
    
    prototype._getCacheEntries = function() {
      const entries: any[] = [];
      cache.forEach((entry, key) => {
        entries.push({
          key,
          age: Date.now() - entry.timestamp,
          hits: entry.hits,
          value: entry.value
        });
      });
      return entries;
    };
    
    return constructor;
  };
}

// 🔑 Cache key parameter decorator
function CacheKey(target: any, propertyKey: string | symbol, parameterIndex: number) {
  const existingKeys = Reflect.getMetadata(CACHE_KEY_METADATA, target, propertyKey) || [];
  existingKeys.push(parameterIndex);
  Reflect.defineMetadata(CACHE_KEY_METADATA, existingKeys, target, propertyKey);
}

// 🗑️ Cache clear method decorator
function CacheClear(methodName: string) {
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = async function(...args: any[]) {
      // Call original method
      const result = await originalMethod.apply(this, args);
      
      // Clear cache for specified method
      if (this._clearCache) {
        this._clearCache(methodName);
      }
      
      return result;
    };
    
    return descriptor;
  };
}

// 📊 Cache statistics property decorator
function CacheStats(target: any, propertyKey: string) {
  Object.defineProperty(target, propertyKey, {
    get() {
      return this._getCacheStats ? this._getCacheStats() : null;
    },
    enumerable: true,
    configurable: true
  });
}

// 🌤️ Example implementation
@Cacheable({ ttl: 5000, maxSize: 100 })
class WeatherService {
  @CacheStats
  stats!: CacheStatistics;
  
  async getWeather(@CacheKey city: string, details: boolean = false): Promise<any> {
    console.log(`🌤️ Fetching weather for ${city}...`);
    // Simulate API call
    await new Promise(resolve => setTimeout(resolve, 1000));
    return { 
      city, 
      temp: Math.round(Math.random() * 30), 
      conditions: ['Sunny', 'Cloudy', 'Rainy'][Math.floor(Math.random() * 3)],
      details: details ? { humidity: 65, windSpeed: 10 } : undefined
    };
  }
  
  async getForecast(@CacheKey city: string, @CacheKey days: number = 7): Promise<any> {
    console.log(`📅 Fetching ${days}-day forecast for ${city}...`);
    await new Promise(resolve => setTimeout(resolve, 800));
    
    const forecast = [];
    for (let i = 0; i < days; i++) {
      forecast.push({
        day: i,
        temp: Math.round(Math.random() * 30),
        conditions: ['Sunny', 'Cloudy', 'Rainy'][Math.floor(Math.random() * 3)]
      });
    }
    
    return { city, days, forecast };
  }
  
  @CacheClear('getWeather')
  async updateWeather(city: string, data: any): Promise<void> {
    console.log(`📝 Updating weather for ${city}:`, data);
    // This will automatically clear cache for getWeather method
  }
  
  @CacheClear('getForecast')
  async updateForecast(city: string, data: any): Promise<void> {
    console.log(`📝 Updating forecast for ${city}`);
  }
  
  // Expose cache entries for debugging
  getCacheInfo(): any {
    return (this as any)._getCacheEntries();
  }
}

// 💫 Advanced example with custom key generator
@Cacheable({ 
  ttl: 10000,
  key: (userId: string, filters: any) => `user:${userId}:${filters.type || 'all'}`
})
class UserDataService {
  @CacheStats
  stats!: CacheStatistics;
  
  async getUserData(userId: string, filters: { type?: string; limit?: number } = {}): Promise<any> {
    console.log(`👤 Fetching data for user ${userId} with filters:`, filters);
    await new Promise(resolve => setTimeout(resolve, 500));
    
    return {
      userId,
      type: filters.type || 'all',
      data: Array(filters.limit || 10).fill(null).map((_, i) => ({
        id: i,
        value: Math.random()
      }))
    };
  }
}

// 🧪 Testing the cache system
async function testCacheSystem() {
  console.log('=== Weather Service Cache Test ===\n');
  
  const weather = new WeatherService();
  
  // First calls - cache miss
  await weather.getWeather('London');
  await weather.getWeather('Paris');
  await weather.getForecast('London', 3);
  
  // Second calls - cache hit
  await weather.getWeather('London');
  await weather.getWeather('Paris');
  await weather.getForecast('London', 3);
  
  // Check stats
  console.log('\n📊 Cache Statistics:', weather.stats);
  console.log('📦 Cache Entries:', weather.getCacheInfo());
  
  // Update and clear cache
  await weather.updateWeather('London', { temp: 25 });
  
  // This should miss cache
  await weather.getWeather('London');
  
  console.log('\n📊 Final Statistics:', weather.stats);
  
  // Test user data service
  console.log('\n\n=== User Data Service Cache Test ===\n');
  
  const userData = new UserDataService();
  
  // Test custom key generation
  await userData.getUserData('123', { type: 'posts' });
  await userData.getUserData('123', { type: 'comments' });
  await userData.getUserData('123', { type: 'posts' }); // Cache hit
  
  console.log('\n📊 User Data Statistics:', userData.stats);
}

testCacheSystem();

🎯 Summary

You’ve mastered class decorators in TypeScript! 🎉 You learned how to:

  • 🏗️ Create powerful class decorators that transform entire classes
  • 🎨 Modify constructors and prototypes dynamically
  • 🧩 Implement mixin patterns using decorators
  • 📊 Build validation and schema systems
  • 💾 Create ORM-style entity decorators
  • ✨ Design real-world patterns like caching and component systems

Class decorators provide elegant ways to enhance your classes with cross-cutting concerns, keeping your code clean and maintainable!

Keep building amazing things with class decorators! 🚀