TypeScript ミックスイン(Mixin)
ミックスイン(Mixin)はコード再利用のパターンで、複数の独立した機能モジュールを1つのクラスに混入させるために使用されます。
TypeScript は、ジェネリック関数が拡張クラスを返す方法で Mixin を実装し、単一継承では複数のソースの振る舞いを再利用できない欠点を補います。
SVG 図: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
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
// 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(クローン後に独立して変更)
// 機能インターフェースを定義
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 プロパティがない
// 制約:基本クラスには 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 の適用範囲を制限でき、コンパイル時に誤った使用を防げる - 適用シーン:ログ、シリアライズ、クローン、タイムスタンプなどの横断的関心事では、多層継承よりも優れている