TypeScript ミックスイン(Mixin)

ミックスイン(Mixin)はコード再利用のパターンで、複数の独立した機能モジュールを1つのクラスに混入させるために使用されます。

TypeScript は、ジェネリック関数が拡張クラスを返す方法で Mixin を実装し、単一継承では複数のソースの振る舞いを再利用できない欠点を補います。

SVG 図:Mixin の組み合わせプロセス 背景 タイトル Mixin の組み合わせプロセス 基本クラス User User クラス Mixin1 への矢印 SerializableMixin + serialize() Mixin2 への矢印 TimestampedMixin + timestamp 結果への矢印 組み合わせ結果 拡張後のクラス 下の説明 Mixin の動作原理 原理の説明1 1. Mixin は関数である クラスを受け取り、拡張された新しいクラスを返す 原理の説明2 2. 変更ではなく継承 extends による継承で、元のクラスは変更しない 原理の説明3 3. 重ねて組み合わせ可能 複数の Mixin をネストして組み合わせて使用できる 矢印マーク

基本概念

Mixin の核となるのは、基本クラスを受け取り拡張クラスを返すジェネリック関数です。Constructor型制約は「継承可能な任意のクラス」を記述するために使用されます。

例

// Constructor 型:任意のインスタンス化可能なクラスを記述
type Constructor<T = {}> = new (...args: any[]) => T;

// Mixin 関数:基本クラスを受け取り、拡張された新しいクラスを返す
function Timestamped<TBase extends Constructor>(Base: TBase) {
    return class extends Base {
        createdAt = new Date();
    };
}

// 基本クラス
class User {
    constructor(public name: string) {}
}

// 混入後に新しいクラスを取得
const TimestampedUser = Timestamped(User);
const user = new TimestampedUser("Alice");

console.log(user.name);                        // Alice
console.log(user.createdAt instanceof Date);   // true

実行結果:

Alice
true

注意:Mixin は元のクラスを変更せず、新しい拡張クラスを返します。元のUserクラスは影響を受けません。


複数の Mixin を組み合わせる

複数の Mixin 関数をネストして呼び出すことで、複数の機能を順番に同じクラスへ重ねることができます。

例

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

// Mixin 1:タイムスタンプを追加
function Timestamped<TBase extends Constructor>(Base: TBase) {
    return class extends Base {
        createdAt = new Date();
    };
}

// Mixin 2:シリアライズを追加
function Serializable<TBase extends Constructor>(Base: TBase) {
    return class extends Base {
        serialize(): string {
            return JSON.stringify(this);
        }
    };
}

// Mixin 3:ログを追加(serialize メソッドに依存)
function Loggable<TBase extends Constructor<{ serialize(): string }>>(Base: TBase) {
    return class extends Base {
        log(): void {
            console.log("[LOG]", this.serialize());
        }
    };
}

class Product {
    constructor(public name: string, public price: number) {}
}

// 3つの Mixin を順番に重ねる
const AdvancedProduct = Loggable(Serializable(Timestamped(Product)));

const p = new AdvancedProduct("Phone", 999);
p.log();
console.log(p.createdAt instanceof Date);  // true

実行結果:

[LOG] {"name":"Phone","price":999,"createdAt":"2026-..."}
true

Mixin とインターフェースの組み合わせ

Mixin が返すクラスはインターフェースの実装を宣言でき、利用側は具体的な実装に依存せずに、インターフェース型を介して混入後のオブジェクトを操作できます。

例

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

// 機能インターフェースを定義
interface ISerializable {
    serialize(): string;
}

interface ICloneable<T> {
    clone(): T;
}

// Mixin がインターフェースを実装
function Serializable<TBase extends Constructor>(Base: TBase) {
    return class extends Base implements ISerializable {
        serialize(): string {
            return JSON.stringify(this);
        }
    };
}

function Cloneable<TBase extends Constructor>(Base: TBase) {
    return class extends Base {
        clone() {
            return Object.assign(
                Object.create(Object.getPrototypeOf(this)),
                this
            );
        }
    };
}

class Article {
    constructor(public title: string, public content: string) {}
}

const RichArticle = Cloneable(Serializable(Article));

const a1 = new RichArticle("TypeScript 入門", "本文内容...");
const a2 = a1.clone();
a2.title = "TypeScript 応用";

console.log(a1.serialize());
// {"title":"TypeScript 入门","content":"正文内容..."}

console.log(a2.serialize());
// {"title":"TypeScript 进阶","content":"正文内容..."}

console.log(a1.title === a2.title);  // false(クローン後に独立して変更)

実行結果:

{"title":"TypeScript 入门","content":"正文内容..."}
{"title":"TypeScript 进阶","content":"正文内容..."}
false

制約付き Mixin

ジェネリック制約を使用することで、Mixin が特定の条件を満たすクラスにのみ混入できるように制限し、実行時に必要なプロパティが不足するのを防ぎます。

例

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

// 制約:基本クラスには id と name プロパティが必要
type WithIdAndName = Constructor<{ id: number; name: string }>;

function Printable<TBase extends WithIdAndName>(Base: TBase) {
    return class extends Base {
        print(): void {
            console.log(`[${this.id}] ${this.name}`);
        }
    };
}

class Item {
    constructor(public id: number, public name: string) {}
}

// 正しい:Item は制約を満たす
const PrintableItem = Printable(Item);
const item = new PrintableItem(42, "Keyboard");
item.print();  // [42] Keyboard

// 誤った例(コンパイラが阻止します):
// class NoId { constructor(public name: string) {} }
// const Bad = Printable(NoId); // エラー:NoId に id プロパティがない

実行結果:

[42] Keyboard

Mixin と継承の比較

観点 継承(extends) Mixin
ソースの数1つの親クラスしか継承できない任意の数を組み合わせ可能
結合度子クラスと親クラスが強い結合各 Mixin は独立しており、結合が低い
再利用の粒度クラス全体の機能を再利用必要に応じて単一機能を再利用
型安全性ネイティブサポートジェネリック制約によって保証する必要がある
適用シーン強い「is-a」関係横断的関心事(ログ、シリアライズ、キャッシュなど)

まとめ

  • 中核パターン:Mixin は基本クラスを受け取り拡張クラスを返すジェネリック関数であり、Constructor<T>は標準の制約型
  • 機能の重ね合わせ:複数の Mixin 関数をネストして呼び出すだけで、複数の機能を同じ1つのクラスに組み合わせられる
  • インターフェースとの組み合わせ:Mixin が返すクラスはインターフェースを実装でき、利用側は具象クラスではなくインターフェースにのみ依存すればよい
  • ジェネリック制約:制約によりTBaseMixin の適用範囲を制限でき、コンパイル時に誤った使用を防げる
  • 適用シーン:ログ、シリアライズ、クローン、タイムスタンプなどの横断的関心事では、多層継承よりも優れている
その他の拡張