+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 51 of 354

๐Ÿงฉ Mixins in TypeScript: Composing Classes

Master mixins in TypeScript to compose functionality from multiple sources and create flexible, reusable class hierarchies ๐Ÿš€

๐Ÿ’ŽAdvanced
30 min read

Prerequisites

  • Strong understanding of classes and inheritance ๐Ÿ“
  • Generic types knowledge ๐Ÿ”
  • Interface and type manipulation skills ๐Ÿ’ป

What you'll learn

  • Understand mixin patterns and composition ๐ŸŽฏ
  • Create reusable mixin functions ๐Ÿ—๏ธ
  • Implement multiple inheritance patterns ๐Ÿ›ก๏ธ
  • Build complex class hierarchies with mixins โœจ

๐ŸŽฏ Introduction

Welcome to the powerful world of mixins! ๐ŸŽ‰ In this guide, weโ€™ll explore how TypeScript enables composition over inheritance through mixins - a pattern that lets you build classes from reusable components.

Youโ€™ll discover how mixins are like LEGO blocks ๐Ÿงฉ - you can combine different pieces to build exactly what you need! Whether youโ€™re avoiding the limitations of single inheritance ๐Ÿšซ, sharing functionality across unrelated classes ๐ŸŒ, or building plugin architectures ๐Ÿ”Œ, understanding mixins is essential for flexible TypeScript design.

By the end of this tutorial, youโ€™ll be confidently composing classes from multiple sources and creating maintainable, reusable code architectures! Letโ€™s start mixing! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Mixins

๐Ÿค” What are Mixins?

Mixins are a pattern for building classes from reusable components. Instead of using inheritance to share functionality, mixins allow you to compose classes by combining multiple partial class implementations. Itโ€™s like having multiple inheritance without the complexity!

Think of mixins like:

  • ๐Ÿงฉ LEGO blocks: Snap together different pieces
  • ๐ŸŽจ Color mixing: Combine colors to create new ones
  • ๐Ÿน Cocktail mixing: Blend ingredients for the perfect drink
  • ๐Ÿงฌ DNA combination: Mixing traits from multiple sources

๐Ÿ’ก Why Use Mixins?

Hereโ€™s why developers love mixins:

  1. Composition over Inheritance ๐Ÿ—๏ธ: More flexible than single inheritance
  2. Reusability โ™ป๏ธ: Share functionality across unrelated classes
  3. Modularity ๐Ÿ“ฆ: Keep concerns separated
  4. Avoiding Diamond Problem ๐Ÿ’Ž: No ambiguity in method resolution

Real-world example: Game development ๐ŸŽฎ - A character might need Moveable, Attackable, and Healable behaviors, but not all characters need all behaviors!

๐Ÿ”ง Basic Mixin Patterns

๐Ÿ“ Function-Based Mixins

Letโ€™s start with the fundamental mixin pattern:

// ๐ŸŽฏ Basic mixin pattern
type Constructor<T = {}> = new (...args: any[]) => T;

// ๐Ÿƒโ€โ™‚๏ธ Moveable mixin
function Moveable<TBase extends Constructor>(Base: TBase) {
  return class Moveable extends Base {
    private _x: number = 0;
    private _y: number = 0;
    private _speed: number = 5;
    
    get position() {
      return { x: this._x, y: this._y };
    }
    
    moveTo(x: number, y: number): void {
      this._x = x;
      this._y = y;
      console.log(`Moved to (${x}, ${y})`);
    }
    
    moveBy(dx: number, dy: number): void {
      this._x += dx * this._speed;
      this._y += dy * this._speed;
      console.log(`Moved by (${dx}, ${dy}) to (${this._x}, ${this._y})`);
    }
    
    setSpeed(speed: number): void {
      this._speed = speed;
    }
  };
}

// โš”๏ธ Attackable mixin
function Attackable<TBase extends Constructor>(Base: TBase) {
  return class Attackable extends Base {
    private _attackPower: number = 10;
    private _attackRange: number = 1;
    
    attack(target: any): void {
      if (this.isInRange(target)) {
        console.log(`Attacking with power ${this._attackPower}`);
        if ('takeDamage' in target) {
          target.takeDamage(this._attackPower);
        }
      } else {
        console.log('Target out of range!');
      }
    }
    
    setAttackPower(power: number): void {
      this._attackPower = power;
    }
    
    setAttackRange(range: number): void {
      this._attackRange = range;
    }
    
    private isInRange(target: any): boolean {
      if ('position' in this && 'position' in target) {
        const myPos = (this as any).position;
        const targetPos = target.position;
        const distance = Math.sqrt(
          Math.pow(targetPos.x - myPos.x, 2) + 
          Math.pow(targetPos.y - myPos.y, 2)
        );
        return distance <= this._attackRange;
      }
      return true;
    }
  };
}

// ๐Ÿ›ก๏ธ Defendable mixin
function Defendable<TBase extends Constructor>(Base: TBase) {
  return class Defendable extends Base {
    private _health: number = 100;
    private _maxHealth: number = 100;
    private _defense: number = 5;
    
    get health() {
      return this._health;
    }
    
    get isAlive() {
      return this._health > 0;
    }
    
    takeDamage(damage: number): void {
      const actualDamage = Math.max(0, damage - this._defense);
      this._health = Math.max(0, this._health - actualDamage);
      console.log(`Took ${actualDamage} damage. Health: ${this._health}/${this._maxHealth}`);
      
      if (!this.isAlive) {
        this.onDeath();
      }
    }
    
    heal(amount: number): void {
      this._health = Math.min(this._maxHealth, this._health + amount);
      console.log(`Healed ${amount}. Health: ${this._health}/${this._maxHealth}`);
    }
    
    setDefense(defense: number): void {
      this._defense = defense;
    }
    
    protected onDeath(): void {
      console.log('Character has died!');
    }
  };
}

// ๐ŸŽฎ Base character class
class Character {
  constructor(public name: string) {
    console.log(`Created character: ${name}`);
  }
  
  greet(): void {
    console.log(`Hello, I'm ${this.name}`);
  }
}

// ๐Ÿ—๏ธ Compose different character types
class Warrior extends Moveable(Attackable(Defendable(Character))) {
  constructor(name: string) {
    super(name);
    this.setAttackPower(15);
    this.setDefense(10);
  }
  
  specialAbility(): void {
    console.log(`${this.name} uses Berserker Rage!`);
    this.setAttackPower(30);
    setTimeout(() => this.setAttackPower(15), 5000);
  }
}

class Scout extends Moveable(Defendable(Character)) {
  constructor(name: string) {
    super(name);
    this.setSpeed(10); // Scouts are fast!
    this.setDefense(3); // But less tanky
  }
  
  stealth(): void {
    console.log(`${this.name} enters stealth mode`);
  }
}

// ๐Ÿ’ซ Usage
const warrior = new Warrior('Thorin');
const scout = new Scout('Legolas');

warrior.greet(); // "Hello, I'm Thorin"
warrior.moveTo(10, 20);
warrior.attack(scout); // Will be out of range
warrior.moveBy(1, 0); // Move closer
warrior.attack(scout); // Now in range!

scout.heal(10);
scout.moveBy(5, 5); // Scouts move faster
scout.stealth();

๐ŸŽจ Constrained Mixins

Creating mixins with type constraints:

// ๐ŸŽฏ Mixins with constraints
interface Timestamped {
  timestamp: Date;
}

interface Tagged {
  tags: Set<string>;
}

// ๐Ÿ“… Timestamped mixin
function TimestampedMixin<TBase extends Constructor>(Base: TBase) {
  return class Timestamped extends Base {
    timestamp = new Date();
    
    updateTimestamp(): void {
      this.timestamp = new Date();
    }
    
    getAge(): number {
      return Date.now() - this.timestamp.getTime();
    }
    
    getFormattedTime(): string {
      return this.timestamp.toISOString();
    }
  };
}

// ๐Ÿท๏ธ Tagged mixin with constraint
function TaggedMixin<TBase extends Constructor<Timestamped>>(Base: TBase) {
  return class Tagged extends Base {
    tags = new Set<string>();
    
    addTag(tag: string): void {
      this.tags.add(tag);
      this.updateTimestamp(); // Can access Timestamped methods!
      console.log(`Added tag "${tag}" at ${this.getFormattedTime()}`);
    }
    
    removeTag(tag: string): boolean {
      const removed = this.tags.delete(tag);
      if (removed) {
        this.updateTimestamp();
      }
      return removed;
    }
    
    hasTag(tag: string): boolean {
      return this.tags.has(tag);
    }
    
    getTags(): string[] {
      return Array.from(this.tags);
    }
  };
}

// ๐Ÿ” Searchable mixin requiring Tagged
function SearchableMixin<TBase extends Constructor<Tagged & Timestamped>>(Base: TBase) {
  return class Searchable extends Base {
    matches(query: string): boolean {
      // Search in tags
      for (const tag of this.tags) {
        if (tag.toLowerCase().includes(query.toLowerCase())) {
          return true;
        }
      }
      return false;
    }
    
    matchesAll(queries: string[]): boolean {
      return queries.every(query => this.matches(query));
    }
    
    matchesAny(queries: string[]): boolean {
      return queries.some(query => this.matches(query));
    }
    
    getRelevanceScore(query: string): number {
      let score = 0;
      const lowerQuery = query.toLowerCase();
      
      for (const tag of this.tags) {
        const lowerTag = tag.toLowerCase();
        if (lowerTag === lowerQuery) score += 10;
        else if (lowerTag.includes(lowerQuery)) score += 5;
      }
      
      // Boost recent items
      const ageInHours = this.getAge() / (1000 * 60 * 60);
      if (ageInHours < 1) score += 5;
      else if (ageInHours < 24) score += 3;
      else if (ageInHours < 168) score += 1;
      
      return score;
    }
  };
}

// ๐Ÿ“„ Document class using constrained mixins
class Document {
  constructor(public title: string, public content: string) {}
}

class SmartDocument extends SearchableMixin(TaggedMixin(TimestampedMixin(Document))) {
  private _version: number = 1;
  
  updateContent(content: string): void {
    this.content = content;
    this._version++;
    this.updateTimestamp();
    this.addTag(`v${this._version}`);
  }
  
  getInfo(): string {
    return `${this.title} (v${this._version}) - ${this.getTags().length} tags - ${this.getFormattedTime()}`;
  }
}

// ๐Ÿ’ซ Usage
const doc = new SmartDocument('TypeScript Guide', 'Learn TypeScript...');
doc.addTag('programming');
doc.addTag('typescript');
doc.addTag('tutorial');

console.log(doc.matches('type')); // true
console.log(doc.getRelevanceScore('typescript')); // High score
console.log(doc.getInfo());

doc.updateContent('Updated content...');
console.log(doc.getTags()); // Includes version tags

๐Ÿš€ Advanced Mixin Patterns

๐Ÿ—๏ธ Parameterized Mixins

Creating configurable mixins:

// ๐ŸŽฏ Parameterized mixin factory
interface CacheConfig {
  maxSize?: number;
  ttl?: number; // Time to live in milliseconds
  strategy?: 'lru' | 'fifo';
}

function Cacheable<TBase extends Constructor>(
  Base: TBase,
  config: CacheConfig = {}
) {
  const { maxSize = 100, ttl = 60000, strategy = 'lru' } = config;
  
  return class Cacheable extends Base {
    private cache = new Map<string, { value: any; timestamp: number; hits: number }>();
    private accessOrder: string[] = [];
    
    protected cacheGet<T>(key: string): T | undefined {
      const entry = this.cache.get(key);
      if (!entry) return undefined;
      
      // Check TTL
      if (Date.now() - entry.timestamp > ttl) {
        this.cache.delete(key);
        return undefined;
      }
      
      // Update access tracking
      entry.hits++;
      if (strategy === 'lru') {
        const index = this.accessOrder.indexOf(key);
        if (index > -1) {
          this.accessOrder.splice(index, 1);
        }
        this.accessOrder.push(key);
      }
      
      return entry.value as T;
    }
    
    protected cacheSet(key: string, value: any): void {
      // Evict if necessary
      if (this.cache.size >= maxSize) {
        this.evict();
      }
      
      this.cache.set(key, {
        value,
        timestamp: Date.now(),
        hits: 0
      });
      
      if (strategy === 'fifo' || strategy === 'lru') {
        this.accessOrder.push(key);
      }
    }
    
    protected cacheClear(): void {
      this.cache.clear();
      this.accessOrder = [];
    }
    
    protected getCacheStats() {
      const entries = Array.from(this.cache.entries());
      const totalHits = entries.reduce((sum, [_, entry]) => sum + entry.hits, 0);
      
      return {
        size: this.cache.size,
        maxSize,
        strategy,
        ttl,
        totalHits,
        hitRate: totalHits / Math.max(1, entries.length),
        oldestEntry: entries.length > 0 
          ? new Date(Math.min(...entries.map(([_, e]) => e.timestamp)))
          : null
      };
    }
    
    private evict(): void {
      if (strategy === 'lru' || strategy === 'fifo') {
        const keyToRemove = this.accessOrder.shift();
        if (keyToRemove) {
          this.cache.delete(keyToRemove);
        }
      }
    }
  };
}

// ๐ŸŒ API client with caching
class APIClient {
  constructor(private baseURL: string) {}
  
  protected async fetch(endpoint: string): Promise<any> {
    console.log(`Fetching: ${this.baseURL}${endpoint}`);
    // Simulate API call
    return { data: `Response from ${endpoint}`, timestamp: Date.now() };
  }
}

// Small cache with short TTL
class CachedAPIClient extends Cacheable(APIClient, { maxSize: 50, ttl: 5000 }) {
  async get(endpoint: string): Promise<any> {
    const cached = this.cacheGet<any>(endpoint);
    if (cached) {
      console.log(`Cache hit: ${endpoint}`);
      return cached;
    }
    
    const data = await this.fetch(endpoint);
    this.cacheSet(endpoint, data);
    return data;
  }
  
  getStats() {
    return this.getCacheStats();
  }
}

// Large cache with long TTL
class DataStore extends Cacheable(APIClient, { maxSize: 1000, ttl: 3600000, strategy: 'lru' }) {
  async loadData(key: string): Promise<any> {
    const cached = this.cacheGet<any>(key);
    if (cached) return cached;
    
    const data = await this.fetch(`/data/${key}`);
    this.cacheSet(key, data);
    return data;
  }
}

๐ŸŽญ Multiple Mixin Composition

Advanced patterns for composing multiple mixins:

// ๐ŸŽฏ Event emitter mixin
type EventMap = Record<string, any[]>;

function EventEmitterMixin<TBase extends Constructor>(Base: TBase) {
  return class EventEmitter extends Base {
    private listeners = new Map<string, Set<Function>>();
    
    on(event: string, handler: Function): void {
      if (!this.listeners.has(event)) {
        this.listeners.set(event, new Set());
      }
      this.listeners.get(event)!.add(handler);
    }
    
    off(event: string, handler: Function): void {
      this.listeners.get(event)?.delete(handler);
    }
    
    emit(event: string, ...args: any[]): void {
      this.listeners.get(event)?.forEach(handler => {
        handler.apply(this, args);
      });
    }
    
    once(event: string, handler: Function): void {
      const wrappedHandler = (...args: any[]) => {
        handler.apply(this, args);
        this.off(event, wrappedHandler);
      };
      this.on(event, wrappedHandler);
    }
  };
}

// ๐Ÿ”„ State machine mixin
interface StateConfig<TStates extends string> {
  initial: TStates;
  transitions: Record<TStates, TStates[]>;
}

function StateMachineMixin<
  TBase extends Constructor,
  TStates extends string
>(Base: TBase, config: StateConfig<TStates>) {
  return class StateMachine extends Base {
    private currentState: TStates = config.initial;
    private stateHistory: TStates[] = [config.initial];
    
    getState(): TStates {
      return this.currentState;
    }
    
    canTransition(to: TStates): boolean {
      const allowedTransitions = config.transitions[this.currentState];
      return allowedTransitions?.includes(to) ?? false;
    }
    
    transition(to: TStates): boolean {
      if (!this.canTransition(to)) {
        console.warn(`Invalid transition from ${this.currentState} to ${to}`);
        return false;
      }
      
      const from = this.currentState;
      this.currentState = to;
      this.stateHistory.push(to);
      
      // Emit event if available
      if ('emit' in this) {
        (this as any).emit('stateChange', { from, to });
      }
      
      return true;
    }
    
    getStateHistory(): TStates[] {
      return [...this.stateHistory];
    }
    
    resetState(): void {
      this.currentState = config.initial;
      this.stateHistory = [config.initial];
    }
  };
}

// ๐Ÿšฆ Observable mixin
function ObservableMixin<TBase extends Constructor>(Base: TBase) {
  return class Observable extends Base {
    private observers = new Map<string, Set<(value: any) => void>>();
    private values = new Map<string, any>();
    
    observe<T>(property: string, callback: (value: T) => void): () => void {
      if (!this.observers.has(property)) {
        this.observers.set(property, new Set());
      }
      
      this.observers.get(property)!.add(callback);
      
      // Call with current value if exists
      if (this.values.has(property)) {
        callback(this.values.get(property));
      }
      
      // Return unsubscribe function
      return () => {
        this.observers.get(property)?.delete(callback);
      };
    }
    
    protected notify(property: string, value: any): void {
      this.values.set(property, value);
      this.observers.get(property)?.forEach(callback => {
        callback(value);
      });
    }
  };
}

// ๐ŸŽฎ Game character with multiple mixins
type CharacterState = 'idle' | 'moving' | 'attacking' | 'defending' | 'dead';

class BaseCharacter {
  constructor(public name: string, public type: string) {}
}

// Compose all mixins
class GameCharacter extends ObservableMixin(
  EventEmitterMixin(
    StateMachineMixin(
      Moveable(
        Attackable(
          Defendable(BaseCharacter)
        )
      ),
      {
        initial: 'idle' as CharacterState,
        transitions: {
          idle: ['moving', 'attacking', 'defending'],
          moving: ['idle', 'attacking', 'defending'],
          attacking: ['idle', 'moving'],
          defending: ['idle', 'moving'],
          dead: []
        }
      }
    )
  )
) {
  constructor(name: string, type: string) {
    super(name, type);
    
    // Set up state change notifications
    this.on('stateChange', ({ from, to }) => {
      console.log(`${this.name} transitioned from ${from} to ${to}`);
      this.notify('state', to);
    });
    
    // Override death handler
    this.on('death', () => {
      this.transition('dead' as CharacterState);
    });
  }
  
  // Override methods to integrate state
  moveTo(x: number, y: number): void {
    if (this.getState() === 'dead') {
      console.log(`${this.name} cannot move while dead!`);
      return;
    }
    
    this.transition('moving' as CharacterState);
    super.moveTo(x, y);
    setTimeout(() => {
      if (this.getState() === 'moving') {
        this.transition('idle' as CharacterState);
      }
    }, 1000);
  }
  
  attack(target: any): void {
    if (this.getState() === 'dead') {
      console.log(`${this.name} cannot attack while dead!`);
      return;
    }
    
    if (this.transition('attacking' as CharacterState)) {
      super.attack(target);
      setTimeout(() => {
        if (this.getState() === 'attacking') {
          this.transition('idle' as CharacterState);
        }
      }, 500);
    }
  }
  
  defend(): void {
    if (this.getState() === 'dead') return;
    
    if (this.transition('defending' as CharacterState)) {
      this.setDefense(this.getDefense() * 2);
      setTimeout(() => {
        if (this.getState() === 'defending') {
          this.setDefense(this.getDefense() / 2);
          this.transition('idle' as CharacterState);
        }
      }, 3000);
    }
  }
  
  protected onDeath(): void {
    super.onDeath();
    this.emit('death');
  }
  
  private getDefense(): number {
    // Access private property through any (mixin limitation)
    return (this as any)._defense ?? 5;
  }
}

// ๐Ÿ’ซ Usage with all features
const hero = new GameCharacter('Aragorn', 'Warrior');

// Observe state changes
hero.observe('state', (state: CharacterState) => {
  console.log(`UI Update: ${hero.name} is now ${state}`);
});

// Listen to events
hero.on('stateChange', ({ to }) => {
  if (to === 'attacking') {
    console.log('โš”๏ธ Battle music starts playing!');
  }
});

// Use the character
hero.moveTo(10, 10);
hero.attack(null); // Will fail - not idle
setTimeout(() => {
  hero.attack(null); // Will work - back to idle
  hero.defend();
}, 1500);

๐ŸŽช Property Mixins

๐Ÿ”ง Mixing in Properties

Adding properties and getters/setters via mixins:

// ๐ŸŽฏ Property decorator mixin
interface PropertyDescriptor<T> {
  default?: T;
  validator?: (value: T) => boolean;
  transformer?: (value: T) => T;
}

function WithProperty<
  TBase extends Constructor,
  TProps extends Record<string, PropertyDescriptor<any>>
>(Base: TBase, properties: TProps) {
  const NewClass = class extends Base {
    private _propertyValues = new Map<string, any>();
    private _propertyMetadata = properties;
    
    constructor(...args: any[]) {
      super(...args);
      
      // Initialize default values
      Object.entries(properties).forEach(([key, descriptor]) => {
        if (descriptor.default !== undefined) {
          this._propertyValues.set(key, descriptor.default);
        }
      });
      
      // Create getters and setters
      Object.keys(properties).forEach(key => {
        Object.defineProperty(this, key, {
          get() {
            return this._propertyValues.get(key);
          },
          set(value: any) {
            const descriptor = this._propertyMetadata[key];
            
            // Validate
            if (descriptor.validator && !descriptor.validator(value)) {
              throw new Error(`Invalid value for ${key}: ${value}`);
            }
            
            // Transform
            const finalValue = descriptor.transformer 
              ? descriptor.transformer(value)
              : value;
            
            const oldValue = this._propertyValues.get(key);
            this._propertyValues.set(key, finalValue);
            
            // Notify if available
            if ('emit' in this) {
              (this as any).emit('propertyChange', {
                property: key,
                oldValue,
                newValue: finalValue
              });
            }
          },
          enumerable: true,
          configurable: true
        });
      });
    }
    
    getProperties(): Record<string, any> {
      const result: Record<string, any> = {};
      this._propertyValues.forEach((value, key) => {
        result[key] = value;
      });
      return result;
    }
    
    setProperties(values: Partial<Record<keyof TProps, any>>): void {
      Object.entries(values).forEach(([key, value]) => {
        if (key in this) {
          (this as any)[key] = value;
        }
      });
    }
  };
  
  return NewClass as typeof NewClass & {
    new (...args: any[]): InstanceType<TBase> & {
      [K in keyof TProps]: TProps[K]['default'] extends infer D
        ? D extends undefined ? any : D
        : any;
    };
  };
}

// ๐Ÿ  Example: Product class with validated properties
class Product {
  constructor(public id: string) {}
}

const EnhancedProduct = WithProperty(
  EventEmitterMixin(Product),
  {
    name: {
      default: '',
      validator: (v: string) => v.length > 0 && v.length <= 100,
      transformer: (v: string) => v.trim()
    },
    price: {
      default: 0,
      validator: (v: number) => v >= 0,
      transformer: (v: number) => Math.round(v * 100) / 100
    },
    stock: {
      default: 0,
      validator: (v: number) => v >= 0 && Number.isInteger(v)
    },
    category: {
      default: 'uncategorized',
      validator: (v: string) => ['electronics', 'clothing', 'food', 'uncategorized'].includes(v)
    },
    active: {
      default: true,
      validator: (v: boolean) => typeof v === 'boolean'
    }
  }
);

// ๐Ÿ’ซ Usage
const product = new EnhancedProduct('prod-001');

// Listen to property changes
product.on('propertyChange', ({ property, oldValue, newValue }) => {
  console.log(`${property} changed from ${oldValue} to ${newValue}`);
});

// Use properties with validation
product.name = '  Laptop  '; // Will be trimmed
product.price = 999.999; // Will be rounded to 999.99
product.stock = 10;
product.category = 'electronics';

console.log(product.getProperties());

// This will throw an error
try {
  product.price = -10; // Invalid!
} catch (e) {
  console.error('Validation error:', e.message);
}

๐Ÿ›ก๏ธ Type-Safe Mixin Patterns

๐Ÿ” Ensuring Type Safety

Advanced type patterns for mixins:

// ๐ŸŽฏ Type-safe mixin with method requirements
interface Identifiable {
  id: string;
}

interface Nameable {
  name: string;
}

// Mixin that requires certain methods
function AuditableMixin<
  TBase extends Constructor<Identifiable & Nameable>
>(Base: TBase) {
  return class Auditable extends Base {
    private auditLog: Array<{
      action: string;
      timestamp: Date;
      details?: any;
    }> = [];
    
    protected audit(action: string, details?: any): void {
      this.auditLog.push({
        action,
        timestamp: new Date(),
        details: {
          ...details,
          entityId: this.id,
          entityName: this.name
        }
      });
    }
    
    getAuditLog() {
      return [...this.auditLog];
    }
    
    getLastAudit() {
      return this.auditLog[this.auditLog.length - 1];
    }
    
    clearAuditLog(): void {
      const count = this.auditLog.length;
      this.auditLog = [];
      this.audit('audit_cleared', { entriesRemoved: count });
    }
  };
}

// ๐Ÿ” Permission mixin with type constraints
type Permission = string;
type Role = {
  name: string;
  permissions: Set<Permission>;
};

interface Authorizable {
  roles: Set<Role>;
}

function AuthorizationMixin<
  TBase extends Constructor<Identifiable & Auditable>
>(Base: TBase) {
  type Auditable = InstanceType<ReturnType<typeof AuditableMixin>>;
  
  return class Authorization extends Base implements Authorizable {
    roles = new Set<Role>();
    
    addRole(role: Role): void {
      this.roles.add(role);
      this.audit('role_added', { role: role.name });
    }
    
    removeRole(role: Role): boolean {
      const removed = this.roles.delete(role);
      if (removed) {
        this.audit('role_removed', { role: role.name });
      }
      return removed;
    }
    
    hasPermission(permission: Permission): boolean {
      for (const role of this.roles) {
        if (role.permissions.has(permission)) {
          return true;
        }
      }
      return false;
    }
    
    checkPermission(permission: Permission): void {
      if (!this.hasPermission(permission)) {
        this.audit('permission_denied', { permission });
        throw new Error(`Permission denied: ${permission}`);
      }
      this.audit('permission_granted', { permission });
    }
    
    getPermissions(): Set<Permission> {
      const allPermissions = new Set<Permission>();
      for (const role of this.roles) {
        role.permissions.forEach(p => allPermissions.add(p));
      }
      return allPermissions;
    }
  };
}

// ๐Ÿข User class with multiple mixins
class User implements Identifiable, Nameable {
  constructor(
    public id: string,
    public name: string,
    public email: string
  ) {}
}

class SecureUser extends AuthorizationMixin(AuditableMixin(User)) {
  private data = new Map<string, any>();
  
  setData(key: string, value: any): void {
    this.checkPermission('data:write');
    this.data.set(key, value);
    this.audit('data_updated', { key });
  }
  
  getData(key: string): any {
    this.checkPermission('data:read');
    this.audit('data_accessed', { key });
    return this.data.get(key);
  }
  
  deleteData(key: string): boolean {
    this.checkPermission('data:delete');
    const deleted = this.data.delete(key);
    this.audit('data_deleted', { key, success: deleted });
    return deleted;
  }
}

// ๐Ÿ’ซ Usage
const adminRole: Role = {
  name: 'admin',
  permissions: new Set(['data:read', 'data:write', 'data:delete'])
};

const readOnlyRole: Role = {
  name: 'readonly',
  permissions: new Set(['data:read'])
};

const user = new SecureUser('user-001', 'Alice', '[email protected]');
user.addRole(readOnlyRole);

// This works
user.getData('someKey');

// This throws an error
try {
  user.setData('key', 'value'); // Permission denied!
} catch (e) {
  console.error(e.message);
}

// Add admin role
user.addRole(adminRole);
user.setData('key', 'value'); // Now it works!

// Check audit log
console.log(user.getAuditLog());

๐ŸŽฎ Hands-On Exercise

Letโ€™s build a game item system using mixins!

๐Ÿ“ Challenge: RPG Item System

Create an item system that:

  1. Uses mixins for different item properties
  2. Supports equipment, consumables, and quest items
  3. Implements rarity and enhancement systems
  4. Maintains type safety throughout
// Your challenge: Implement this item system
type ItemRarity = 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary';
type ItemType = 'weapon' | 'armor' | 'consumable' | 'quest' | 'material';

interface BaseItem {
  id: string;
  name: string;
  type: ItemType;
}

// Mixins to implement:
// 1. Rareable - adds rarity with color coding
// 2. Stackable - allows items to stack with max stack size
// 3. Enhanceable - allows upgrading with enhancement level
// 4. Useable - adds use functionality with cooldowns
// 5. Tradeable - adds trading properties and restrictions
// 6. Bindable - adds binding to character (soulbound)

// Example usage to support:
const sword = new EnhanceableWeapon('sword-001', 'Excalibur');
sword.enhance(); // +1
sword.setRarity('legendary');

const potion = new StackableConsumable('potion-001', 'Health Potion');
potion.stack(5);
potion.use(); // Reduces stack by 1

const questItem = new BindableQuestItem('quest-001', 'Ancient Scroll');
questItem.bindToCharacter('hero-001');
questItem.isBound(); // true

// Implement the mixin system!

๐Ÿ’ก Solution

Click to see the solution
// Base types
type ItemRarity = 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary';
type ItemType = 'weapon' | 'armor' | 'consumable' | 'quest' | 'material';

interface BaseItem {
  id: string;
  name: string;
  type: ItemType;
}

type Constructor<T = {}> = new (...args: any[]) => T;

// ๐ŸŒŸ Rareable mixin
function Rareable<TBase extends Constructor<BaseItem>>(Base: TBase) {
  return class Rareable extends Base {
    private _rarity: ItemRarity = 'common';
    
    get rarity(): ItemRarity {
      return this._rarity;
    }
    
    setRarity(rarity: ItemRarity): void {
      this._rarity = rarity;
    }
    
    getRarityColor(): string {
      const colors: Record<ItemRarity, string> = {
        common: '#808080',
        uncommon: '#00FF00',
        rare: '#0080FF',
        epic: '#B000B0',
        legendary: '#FF8000'
      };
      return colors[this._rarity];
    }
    
    getRarityMultiplier(): number {
      const multipliers: Record<ItemRarity, number> = {
        common: 1,
        uncommon: 1.2,
        rare: 1.5,
        epic: 2,
        legendary: 3
      };
      return multipliers[this._rarity];
    }
    
    getDisplayName(): string {
      return `${this.name} [${this._rarity.toUpperCase()}]`;
    }
  };
}

// ๐Ÿ“ฆ Stackable mixin
function Stackable<TBase extends Constructor<BaseItem>>(
  Base: TBase,
  maxStack: number = 99
) {
  return class Stackable extends Base {
    private _quantity: number = 1;
    
    get quantity(): number {
      return this._quantity;
    }
    
    get maxStackSize(): number {
      return maxStack;
    }
    
    stack(amount: number): boolean {
      const newQuantity = this._quantity + amount;
      if (newQuantity > maxStack || newQuantity < 0) {
        return false;
      }
      this._quantity = newQuantity;
      return true;
    }
    
    split(amount: number): Stackable | null {
      if (amount >= this._quantity || amount <= 0) {
        return null;
      }
      
      this._quantity -= amount;
      const splitItem = new (this.constructor as any)(this.id + '_split', this.name);
      splitItem._quantity = amount;
      return splitItem;
    }
    
    canStackWith(other: any): boolean {
      return other instanceof Stackable &&
             other.id === this.id &&
             other.name === this.name &&
             this._quantity + other.quantity <= maxStack;
    }
    
    mergeStack(other: Stackable): boolean {
      if (!this.canStackWith(other)) return false;
      
      this._quantity += other.quantity;
      other._quantity = 0;
      return true;
    }
  };
}

// โš”๏ธ Enhanceable mixin
function Enhanceable<TBase extends Constructor<BaseItem>>(Base: TBase) {
  return class Enhanceable extends Base {
    private _enhancementLevel: number = 0;
    private _maxEnhancement: number = 10;
    private _enhancementHistory: Array<{ level: number; success: boolean; timestamp: Date }> = [];
    
    get enhancementLevel(): number {
      return this._enhancementLevel;
    }
    
    enhance(): boolean {
      if (this._enhancementLevel >= this._maxEnhancement) {
        console.log('Max enhancement reached!');
        return false;
      }
      
      // Success rate decreases with level
      const successRate = Math.max(0.1, 1 - (this._enhancementLevel * 0.1));
      const success = Math.random() < successRate;
      
      this._enhancementHistory.push({
        level: this._enhancementLevel,
        success,
        timestamp: new Date()
      });
      
      if (success) {
        this._enhancementLevel++;
        console.log(`Enhancement successful! Now +${this._enhancementLevel}`);
      } else {
        console.log('Enhancement failed!');
        // Could break item at high levels
        if (this._enhancementLevel > 7 && Math.random() < 0.1) {
          this._enhancementLevel = Math.max(0, this._enhancementLevel - 1);
          console.log('Item degraded!');
        }
      }
      
      return success;
    }
    
    getEnhancedName(): string {
      return this._enhancementLevel > 0 
        ? `${this.name} +${this._enhancementLevel}`
        : this.name;
    }
    
    getEnhancementBonus(): number {
      return this._enhancementLevel * 0.1; // 10% per level
    }
    
    getEnhancementHistory() {
      return [...this._enhancementHistory];
    }
  };
}

// ๐Ÿพ Useable mixin
function Useable<TBase extends Constructor<BaseItem>>(
  Base: TBase,
  cooldown: number = 0
) {
  return class Useable extends Base {
    private _lastUsed: Date | null = null;
    private _uses: number = 0;
    private _cooldownMs: number = cooldown * 1000;
    
    use(): boolean {
      if (!this.canUse()) {
        const remaining = this.getCooldownRemaining();
        console.log(`On cooldown! ${remaining}ms remaining`);
        return false;
      }
      
      this._lastUsed = new Date();
      this._uses++;
      
      this.onUse();
      
      // Reduce stack if stackable
      if ('quantity' in this && 'stack' in this) {
        (this as any).stack(-1);
      }
      
      return true;
    }
    
    canUse(): boolean {
      if (!this._lastUsed) return true;
      
      const elapsed = Date.now() - this._lastUsed.getTime();
      return elapsed >= this._cooldownMs;
    }
    
    getCooldownRemaining(): number {
      if (!this._lastUsed) return 0;
      
      const elapsed = Date.now() - this._lastUsed.getTime();
      return Math.max(0, this._cooldownMs - elapsed);
    }
    
    getUses(): number {
      return this._uses;
    }
    
    protected onUse(): void {
      console.log(`Used ${this.name}`);
    }
  };
}

// ๐Ÿ’ฐ Tradeable mixin
function Tradeable<TBase extends Constructor<BaseItem>>(Base: TBase) {
  return class Tradeable extends Base {
    private _tradeable: boolean = true;
    private _minLevel: number = 0;
    private _value: number = 0;
    
    get tradeable(): boolean {
      return this._tradeable;
    }
    
    setTradeable(tradeable: boolean): void {
      this._tradeable = tradeable;
    }
    
    get value(): number {
      let baseValue = this._value;
      
      // Adjust for rarity
      if ('getRarityMultiplier' in this) {
        baseValue *= (this as any).getRarityMultiplier();
      }
      
      // Adjust for enhancement
      if ('getEnhancementBonus' in this) {
        baseValue *= (1 + (this as any).getEnhancementBonus());
      }
      
      return Math.floor(baseValue);
    }
    
    setValue(value: number): void {
      this._value = value;
    }
    
    setMinLevel(level: number): void {
      this._minLevel = level;
    }
    
    canTrade(characterLevel: number): boolean {
      return this._tradeable && characterLevel >= this._minLevel;
    }
  };
}

// ๐Ÿ”’ Bindable mixin
function Bindable<TBase extends Constructor<BaseItem>>(Base: TBase) {
  return class Bindable extends Base {
    private _boundTo: string | null = null;
    private _bindOnPickup: boolean = false;
    private _bindOnEquip: boolean = false;
    
    bindToCharacter(characterId: string): boolean {
      if (this._boundTo && this._boundTo !== characterId) {
        return false;
      }
      
      this._boundTo = characterId;
      
      // Make untradeable when bound
      if ('setTradeable' in this) {
        (this as any).setTradeable(false);
      }
      
      return true;
    }
    
    unbind(): boolean {
      if (!this._boundTo) return false;
      
      this._boundTo = null;
      return true;
    }
    
    isBound(): boolean {
      return this._boundTo !== null;
    }
    
    getBoundCharacter(): string | null {
      return this._boundTo;
    }
    
    setBindOnPickup(bind: boolean): void {
      this._bindOnPickup = bind;
    }
    
    setBindOnEquip(bind: boolean): void {
      this._bindOnEquip = bind;
    }
  };
}

// ๐Ÿ—ก๏ธ Item classes
class Item implements BaseItem {
  constructor(
    public id: string,
    public name: string,
    public type: ItemType
  ) {}
}

// Weapon class
class EnhanceableWeapon extends Tradeable(Enhanceable(Rareable(Item))) {
  private _damage: number = 10;
  
  constructor(id: string, name: string) {
    super(id, name, 'weapon');
    this.setValue(100);
  }
  
  getDamage(): number {
    const baseDamage = this._damage;
    const rarityBonus = this.getRarityMultiplier();
    const enhancementBonus = 1 + this.getEnhancementBonus();
    
    return Math.floor(baseDamage * rarityBonus * enhancementBonus);
  }
}

// Consumable class
class StackableConsumable extends Useable(Stackable(Tradeable(Item), 20), 5) {
  constructor(id: string, name: string) {
    super(id, name, 'consumable');
    this.setValue(10);
  }
  
  protected onUse(): void {
    console.log(`Consumed ${this.name}. ${(this as any).quantity - 1} remaining`);
  }
}

// Quest item class
class BindableQuestItem extends Bindable(Rareable(Item)) {
  private _questId: string;
  
  constructor(id: string, name: string, questId: string) {
    super(id, name, 'quest');
    this._questId = questId;
    this.setBindOnPickup(true);
  }
  
  getQuestId(): string {
    return this._questId;
  }
}

// ๐Ÿ’ซ Usage example
console.log('=== RPG Item System Demo ===\n');

// Create legendary sword
const sword = new EnhanceableWeapon('sword-001', 'Excalibur');
sword.setRarity('legendary');
console.log(`Created: ${sword.getDisplayName()}`);
console.log(`Base damage: ${sword.getDamage()}`);

// Enhance it
for (let i = 0; i < 5; i++) {
  sword.enhance();
}
console.log(`Enhanced damage: ${sword.getDamage()}`);
console.log(`Value: ${sword.value} gold\n`);

// Create health potions
const potion = new StackableConsumable('potion-001', 'Health Potion');
potion.stack(4); // Now have 5 potions
console.log(`Created: ${potion.name} x${potion.quantity}`);

// Use potions
potion.use();
console.log(`After use: ${potion.quantity} remaining`);
console.log(`Can use again: ${potion.canUse()} (cooldown: ${potion.getCooldownRemaining()}ms)\n`);

// Create quest item
const questItem = new BindableQuestItem('quest-001', 'Ancient Scroll', 'main-quest-01');
questItem.setRarity('epic');
console.log(`Created: ${questItem.getDisplayName()}`);
questItem.bindToCharacter('hero-001');
console.log(`Bound to: ${questItem.getBoundCharacter()}`);
console.log(`Is bound: ${questItem.isBound()}`);

๐ŸŽฏ Summary

Youโ€™ve mastered mixins in TypeScript! ๐ŸŽ‰ You learned how to:

  • ๐Ÿงฉ Create reusable mixin functions
  • ๐Ÿ—๏ธ Compose classes from multiple sources
  • ๐ŸŽจ Use parameterized and constrained mixins
  • ๐Ÿ” Maintain type safety with complex compositions
  • ๐ŸŽญ Implement multiple inheritance patterns
  • โœจ Build flexible, maintainable architectures

Mixins provide a powerful alternative to traditional inheritance, enabling you to build complex class hierarchies through composition rather than inheritance chains!