従来のオブジェクト指向階層に加えて、再利用可能なコンポーネントからクラスを構築するもう1つの人気のある方法は、より単純な部分クラスを組み合わせて構築することです。Scalaなどの言語のミックスインやトレイトの概念に精通しているかもしれません。このパターンはJavaScriptコミュニティでも人気があります。
ミックスインの仕組み
このパターンは、クラス継承でジェネリクスを使用して基底クラスを拡張することに依存しています。TypeScriptのミックスインのサポートは、クラス式パターンによって最適に行われます。このパターンのJavaScriptでの動作機序については、こちらをご覧ください。
まず、ミックスインを適用する基底クラスが必要です。
tsTry
classSprite {name = "";x = 0;y = 0;constructor(name : string) {this.name =name ;}}
次に、型と、基底クラスを拡張するクラス式を返すファクトリ関数が必要です。
tsTry
// 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.typeConstructor = new (...args : any[]) => {};// This mixin adds a scale property, with getters and setters// for changing it with an encapsulated private property:functionScale <TBase extendsConstructor >(Base :TBase ) {return classScaling extendsBase {// Mixins may not declare private/protected properties// however, you can use ES2020 private fields_scale = 1;setScale (scale : number) {this._scale =scale ;}getscale (): number {return this._scale ;}};}
これらがすべて設定されたら、ミックスインが適用された基底クラスを表すクラスを作成できます。
tsTry
// Compose a new class from the Sprite class,// with the Mixin Scale applier:constEightBitSprite =Scale (Sprite );constflappySprite = newEightBitSprite ("Bird");flappySprite .setScale (0.8);console .log (flappySprite .scale );
制約付きミックスイン
上記の形式では、ミックスインはクラスに関する基礎知識を持たないため、目的の設計を作成するのが難しい場合があります。
これをモデル化するために、元のコンストラクター型をジェネリック引数を受け入れるように変更します。
tsTry
// This was our previous constructor:typeConstructor = new (...args : any[]) => {};// Now we use a generic version which can apply a constraint on// the class which this mixin is applied totypeGConstructor <T = {}> = new (...args : any[]) =>T ;
これにより、制約付き基底クラスでのみ機能するクラスを作成できます。
tsTry
typePositionable =GConstructor <{setPos : (x : number,y : number) => void }>;typeSpritable =GConstructor <Sprite >;typeLoggable =GConstructor <{
その後、特定の基底に基づいて構築する場合にのみ機能するミックスインを作成できます。
tsTry
functionJumpable <TBase extendsPositionable >(Base :TBase ) {return classJumpable extendsBase {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);}};}
代替パターン
このドキュメントの以前のバージョンでは、ランタイム階層と型階層の両方を個別に作成し、最後にマージするというミックスインの作成方法を推奨していました。
tsTry
// Each mixin is a traditional ES classclassJumpable {jump () {}}classDuckable {duck () {}}// Including the baseclassSprite {x = 0;y = 0;}// Then you create an interface which merges// the expected mixins with the same name as your baseinterfaceSprite extendsJumpable ,Duckable {}// Apply the mixins into the base class via// the JS at runtimeapplyMixins (Sprite , [Jumpable ,Duckable ]);letplayer = newSprite ();player .jump ();console .log (player .x ,player .y );// This can live anywhere in your codebase:functionapplyMixins (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));});});}
このパターンは、コンパイラへの依存度が低く、コードベースへの依存度が高いため、ランタイムと型システムの両方が正しく同期されていることを確認できます。
制約
ミックスインパターンは、コードフロー分析によってTypeScriptコンパイラ内でネイティブにサポートされています。ネイティブサポートの限界に達する可能性のあるケースがいくつかあります。
デコレータとミックスイン #4881
コードフロー分析を介してデコレータを使用してミックスインを提供することはできません。
tsTry
// A decorator function which replicates the mixin pattern:constPausable = (target : typeofPlayer ) => {return classPausable extendstarget {shouldFreeze = false;};};@Pausable classPlayer {x = 0;y = 0;}// The Player class does not have the decorator's type merged:constplayer = newPlayer ();Property 'shouldFreeze' does not exist on type 'Player'.2339Property 'shouldFreeze' does not exist on type 'Player'.player .; shouldFreeze // The runtime aspect could be manually replicated via// type composition or interface merging.typeFreezablePlayer =Player & {shouldFreeze : boolean };constplayerTwo = (newPlayer () as unknown) asFreezablePlayer ;playerTwo .shouldFreeze ;
静的プロパティミックスイン #17829
制約というよりは、落とし穴です。クラス式パターンはシングルトンを作成するため、型システムでマッピングして異なる変数型をサポートすることはできません。
これを回避するには、ジェネリックに基づいて異なるクラスを返す関数を使用します。
tsTry
functionbase <T >() {classBase {staticprop :T ;}returnBase ;}functionderived <T >() {classDerived extendsbase <T >() {staticanotherProp :T ;}returnDerived ;}classSpec extendsderived <string>() {}Spec .prop ; // stringSpec .anotherProp ; // string