TypeScript Mixins: A Practical Guide to Composable Class Patterns

TypeScript mixins let you add features to classes without the limitations of regular inheritance. They’re a practical way to write cleaner, more maintainable code through composition rather than inheritance. Let’s explore how to use them effectively.

Table of Contents

What are Mixins?

Mixins are patterns that combine multiple classes into one. You can think of them as small, reusable pieces of code that snap together like building blocks to create more complex classes.

The Problem Mixins Solve

Here’s a common situation that shows why mixins are useful:

class Animal {
  move() {
    console.log('Moving...');
  }
}

class SwimmingAnimal extends Animal {
  swim() {
    console.log('Swimming...');
  }
}

class FlyingAnimal extends Animal {
  fly() {
    console.log('Flying...');
  }
}
Code language: JavaScript (javascript)

What happens when we want to create a duck that can both swim and fly? Regular inheritance won’t work here because TypeScript doesn’t allow a class to inherit from multiple parent classes.

Creating Mixins in TypeScript

Here’s the solution using mixins:

// Define the base constructor type
type Constructor = new (...args: any[]) => {};

// Create mixin functions
function Swimming<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    swim() {
      console.log('Swimming...');
    }
  };
}

function Flying<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    fly() {
      console.log('Flying...');
    }
  };
}

// Base class
class Animal {
  move() {
    console.log('Moving...');
  }
}

// Create a duck using mixins
const Duck = Swimming(Flying(Animal));

// Use the composed class
const duck = new Duck();
duck.move();  // Output: Moving...
duck.swim();  // Output: Swimming...
duck.fly();   // Output: Flying...
Code language: JavaScript (javascript)

Making Mixins Type-Safe

Adding interfaces makes your mixins more reliable:

interface ISwimming {
  swim(): void;
}

interface IFlying {
  fly(): void;
}

function Swimming<TBase extends Constructor>(Base: TBase) {
  return class extends Base implements ISwimming {
    swim() {
      console.log('Swimming...');
    }
  };
}

function Flying<TBase extends Constructor>(Base: TBase) {
  return class extends Base implements IFlying {
    fly() {
      console.log('Flying...');
    }
  };
}
Code language: PHP (php)

Managing State in Mixins

Mixins can handle state too:

function WithState<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    private state: any = {};

    setState(newState: any) {
      this.state = { ...this.state, ...newState };
    }

    getState() {
      return this.state;
    }
  };
}
Code language: JavaScript (javascript)

Handling Mixin Dependencies

When mixins need to work together:

function WithLogging<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    protected log(message: string) {
      console.log(`[${new Date().toISOString()}] ${message}`);
    }
  };
}

function WithErrorHandling<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    protected handleError(error: Error) {
      if ('log' in this) {
        (this as any).log(`Error: ${error.message}`);
      }
      // Handle error
    }
  };
}
Code language: JavaScript (javascript)

Smart Mixin Patterns

Configurable Mixins

Make your mixins adaptable with parameters:

function WithTimeout<TBase extends Constructor>(timeout: number) {
  return function(Base: TBase) {
    return class extends Base {
      async executeWithTimeout<T>(task: () => Promise<T>): Promise<T> {
        const timeoutPromise = new Promise((_, reject) => {
          setTimeout(() => reject(new Error('Operation timed out')), timeout);
        });

        return Promise.race([task(), timeoutPromise]) as Promise<T>;
      }
    };
  };
}

// Usage
const WithFiveSecondTimeout = WithTimeout(5000);
class Service extends WithFiveSecondTimeout(Object) {
  async fetchData() {
    return this.executeWithTimeout(async () => {
      // Fetch data
      return 'data';
    });
  }
}
Code language: JavaScript (javascript)

Feature-Toggle Mixins

Create mixins that switch features on or off:

function WithFeatureFlag<TBase extends Constructor>(featureName: string) {
  return function(Base: TBase) {
    return class extends Base {
      private isFeatureEnabled() {
        return process.env[`FEATURE_${featureName.toUpperCase()}`] === 'true';
      }

      protected executeIfEnabled(action: () => void) {
        if (this.isFeatureEnabled()) {
          action();
        }
      }
    };
  };
}
Code language: JavaScript (javascript)

Real-World Examples

UI Component Enhancement

function WithTheme<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    protected theme: 'light' | 'dark' = 'light';

    setTheme(theme: 'light' | 'dark') {
      this.theme = theme;
    }

    getTheme() {
      return this.theme;
    }
  };
}

function WithI18n<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    private locale = 'en';

    setLocale(locale: string) {
      this.locale = locale;
    }

    translate(key: string): string {
      return `Translated: ${key}`;
    }
  };
}
Code language: JavaScript (javascript)

API Client Caching

function WithCache<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    private cache = new Map<string, any>();

    protected async withCache<T>(key: string, getter: () => Promise<T>): Promise<T> {
      if (this.cache.has(key)) {
        return this.cache.get(key);
      }
      const value = await getter();
      this.cache.set(key, value);
      return value;
    }
  };
}
Code language: JavaScript (javascript)

Tips for Success

  1. Keep each mixin focused on one specific task
  2. Pay attention to the order when using multiple mixins
  3. Always use interfaces to make your mixins type-safe
  4. Write clear documentation about how your mixins work together
  5. Test mixins both alone and in combination

Wrapping Up

Mixins are a practical way to share code between classes in TypeScript. They’re more flexible than inheritance and help you write more maintainable code. Start with simple mixins and gradually build up to more complex patterns as you get comfortable with the concept.

Want to learn more about code organization in TypeScript? Check out our guide on TypeScript Decorators vs Methods to see how different patterns compare.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Share via
Copy link
Powered by Social Snap