除了传统的 OO 层次结构之外,另一种流行的从可重用组件构建类的做法是通过组合更简单的部分类来构建它们。您可能熟悉 Scala 等语言的 mixin 或 trait 的概念,这种模式在 JavaScript 社区也越来越流行。
Mixin 如何工作?
这种模式依赖于使用泛型和类继承来扩展基类。TypeScript 最佳的 mixin 支持是通过类表达式模式实现的。您可以阅读更多关于这种模式在 JavaScript 中如何工作的信息 这里。
首先,我们需要一个类,在其之上应用 mixin
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 ;}};}
设置好这些后,就可以创建一个类来表示应用了 mixin 的基类。
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 );
受限 Mixin
在上述形式中,mixin 对类没有底层知识,这可能难以创建你想要的设计。
为了模拟这种情况,我们修改了原始构造函数类型以接受泛型参数。
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 <{
然后,你可以创建仅在你有特定基类可供构建时才起作用的 mixin。
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);}};}
替代模式
本文档的早期版本建议了一种编写 mixin 的方法,你可以在其中分别创建运行时和类型层次结构,然后在最后将它们合并。
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 编译器通过代码流分析原生支持 mixin 模式。在一些情况下,你可能会遇到原生支持的边缘情况。
装饰器和 Mixin #4881
你不能使用装饰器通过代码流分析来提供 mixin。
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 ;
静态属性 Mixin #17829
与其说是一个限制,更像是一个陷阱。类表达式模式创建单例,因此它们无法在类型系统中映射以支持不同的变量类型。
你可以使用函数来返回你的类,这些类根据泛型而有所不同,以此来解决这个问题。
tsTry
functionbase <T >() {classBase {staticprop :T ;}returnBase ;}functionderived <T >() {classDerived extendsbase <T >() {staticanotherProp :T ;}returnDerived ;}classSpec extendsderived <string>() {}Spec .prop ; // stringSpec .anotherProp ; // string