ミックスイン

従来のオブジェクト指向階層に加えて、再利用可能なコンポーネントからクラスを構築するもう1つの人気のある方法は、より単純な部分クラスを組み合わせて構築することです。Scalaなどの言語のミックスインやトレイトの概念に精通しているかもしれません。このパターンはJavaScriptコミュニティでも人気があります。

ミックスインの仕組み

このパターンは、クラス継承でジェネリクスを使用して基底クラスを拡張することに依存しています。TypeScriptのミックスインのサポートは、クラス式パターンによって最適に行われます。このパターンのJavaScriptでの動作機序については、こちらをご覧ください。

まず、ミックスインを適用する基底クラスが必要です。

ts
class Sprite {
name = "";
x = 0;
y = 0;
 
constructor(name: string) {
this.name = name;
}
}
Try

次に、型と、基底クラスを拡張するクラス式を返すファクトリ関数が必要です。

ts
// To get started, we need a type which we'll use to extend
// other classes from. The main responsibility is to declare
// that the type being passed in is a class.
 
type Constructor = new (...args: any[]) => {};
 
// This mixin adds a scale property, with getters and setters
// for changing it with an encapsulated private property:
 
function Scale<TBase extends Constructor>(Base: TBase) {
return class Scaling extends Base {
// Mixins may not declare private/protected properties
// however, you can use ES2020 private fields
_scale = 1;
 
setScale(scale: number) {
this._scale = scale;
}
 
get scale(): number {
return this._scale;
}
};
}
Try

これらがすべて設定されたら、ミックスインが適用された基底クラスを表すクラスを作成できます。

ts
// Compose a new class from the Sprite class,
// with the Mixin Scale applier:
const EightBitSprite = Scale(Sprite);
 
const flappySprite = new EightBitSprite("Bird");
flappySprite.setScale(0.8);
console.log(flappySprite.scale);
Try

制約付きミックスイン

上記の形式では、ミックスインはクラスに関する基礎知識を持たないため、目的の設計を作成するのが難しい場合があります。

これをモデル化するために、元のコンストラクター型をジェネリック引数を受け入れるように変更します。

ts
// This was our previous constructor:
type Constructor = new (...args: any[]) => {};
// Now we use a generic version which can apply a constraint on
// the class which this mixin is applied to
type GConstructor<T = {}> = new (...args: any[]) => T;
Try

これにより、制約付き基底クラスでのみ機能するクラスを作成できます。

ts
type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;
type Spritable = GConstructor<Sprite>;
type Loggable = GConstructor<{ print: () => void }>;
Try

その後、特定の基底に基づいて構築する場合にのみ機能するミックスインを作成できます。

ts
function Jumpable<TBase extends Positionable>(Base: TBase) {
return class Jumpable extends Base {
jump() {
// This mixin will only work if it is passed a base
// class which has setPos defined because of the
// Positionable constraint.
this.setPos(0, 20);
}
};
}
Try

代替パターン

このドキュメントの以前のバージョンでは、ランタイム階層と型階層の両方を個別に作成し、最後にマージするというミックスインの作成方法を推奨していました。

ts
// Each mixin is a traditional ES class
class Jumpable {
jump() {}
}
 
class Duckable {
duck() {}
}
 
// Including the base
class Sprite {
x = 0;
y = 0;
}
 
// Then you create an interface which merges
// the expected mixins with the same name as your base
interface Sprite extends Jumpable, Duckable {}
// Apply the mixins into the base class via
// the JS at runtime
applyMixins(Sprite, [Jumpable, Duckable]);
 
let player = new Sprite();
player.jump();
console.log(player.x, player.y);
 
// This can live anywhere in your codebase:
function applyMixins(derivedCtor: any, constructors: any[]) {
constructors.forEach((baseCtor) => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
Object.defineProperty(
derivedCtor.prototype,
name,
Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
Object.create(null)
);
});
});
}
Try

このパターンは、コンパイラへの依存度が低く、コードベースへの依存度が高いため、ランタイムと型システムの両方が正しく同期されていることを確認できます。

制約

ミックスインパターンは、コードフロー分析によってTypeScriptコンパイラ内でネイティブにサポートされています。ネイティブサポートの限界に達する可能性のあるケースがいくつかあります。

デコレータとミックスイン #4881

コードフロー分析を介してデコレータを使用してミックスインを提供することはできません。

ts
// A decorator function which replicates the mixin pattern:
const Pausable = (target: typeof Player) => {
return class Pausable extends target {
shouldFreeze = false;
};
};
 
@Pausable
class Player {
x = 0;
y = 0;
}
 
// The Player class does not have the decorator's type merged:
const player = new Player();
player.shouldFreeze;
Property 'shouldFreeze' does not exist on type 'Player'.2339Property 'shouldFreeze' does not exist on type 'Player'.
 
// The runtime aspect could be manually replicated via
// type composition or interface merging.
type FreezablePlayer = Player & { shouldFreeze: boolean };
 
const playerTwo = (new Player() as unknown) as FreezablePlayer;
playerTwo.shouldFreeze;
Try

静的プロパティミックスイン #17829

制約というよりは、落とし穴です。クラス式パターンはシングルトンを作成するため、型システムでマッピングして異なる変数型をサポートすることはできません。

これを回避するには、ジェネリックに基づいて異なるクラスを返す関数を使用します。

ts
function base<T>() {
class Base {
static prop: T;
}
return Base;
}
 
function derived<T>() {
class Derived extends base<T>() {
static anotherProp: T;
}
return Derived;
}
 
class Spec extends derived<string>() {}
 
Spec.prop; // string
Spec.anotherProp; // string
Try

TypeScriptのドキュメントはオープンソースプロジェクトです。プルリクエストを送信して、これらのページの改善にご協力ください

このページへの貢献者
OTOrta Therox (16) (Orta Therox)
GMGleb Maksimenko (1) (Gleb Maksimenko)
IOIván Ovejero (1) (Iván Ovejero)
DEDom Eccleston (1) (Dom Eccleston)
OOblosys (1) (Oblosys)
5+

最終更新日: 2024年3月21日