除了传统的面向对象继承体系之外,另一种通过可复用组件构建类的流行方式,是将较简单的部分类组合起来。你可能熟悉 Scala 等语言中的混入或特质(trait)概念,这种模式在 JavaScript 社区中也已经相当流行。
混入是如何工作的?
这种模式依赖将泛型与类继承结合使用来扩展基类。TypeScript 对混入的最佳支持是通过类表达式模式实现的。你可以在这里进一步了解这种模式在 JavaScript 中的工作方式。
首先,我们需要一个类,混入将应用在它之上:
tsTryclassSprite {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 ;
这样就可以创建仅适用于受约束基类的类:
tsTrytypePositionable =GConstructor <{setPos : (x : number,y : number) => void }>;typeSpritable =GConstructor <Sprite >;typeLoggable =GConstructor <{
然后你可以创建仅在拥有特定基类可供继承时才能工作的混入:
tsTryfunctionJumpable <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
与其说是约束,不如说是陷阱。类表达式模式创建的是单例,因此无法在类型系统中映射它们以支持不同的变量类型。
你可以通过使用函数返回基于泛型而有所不同的类来绕过这个问题:
tsTryfunctionbase <T >() {classBase {staticprop :T ;}returnBase ;}functionderived <T >() {classDerived extendsbase <T >() {staticanotherProp :T ;}returnDerived ;}classSpec extendsderived <string>() {}Spec .prop ; // stringSpec .anotherProp ; // string