TypeScript学习(十四):混入

396 阅读2分钟

何为混入

简单地可以理解为,往一个对象( 类 )里面添加一些属性及值。

以下面为例,往 Base 类(对象) 中混入 Scale 属性。

type Constructor = new (...arg: any[]) => {}

// 这里利用 泛型 + extends 对 Base类型进行了收束,必须其为一个类
// 因为只有类才可以被继承
function Scale<TBase extends Constructor>(Base: TBase){
    return class Scaling extends Base{
        _scale = 0
        get Scale(): number{
            return this._scale
        }
        setScale(scale: number){
            this._scale = scalse
        }
    }
}

// 使用
class Sprite {
    name = "";
    x = 0;
    y = 0;
    
    constructor(name:string){
        this.name = name
    }
}
const EightBitSprite = Scale(Sprite);
const ins = new EightBitSprite("Bird");
ins.setScale(0.8)

Constrained Mixins

在混入的过程中,可能需要调用基类特定的方法。而在上述案例中,我们是不能得到基类的潜在(属性)信息。那么,为了开发便利,需要基类具有特定属性的类型。

原理:TS/JS 的构造函数是可以返回一个对象的,利用这种特性,可以“添加” 我们需要访问的特定属性。并且,由于类型是不会侵入JS的,因此这种做法没有问题。

具体如下:

// T 默认为一个空对象
// 这里只是单纯地,利用构造函数可以返回一个对象的特性来限制 混入的对象
// 除 枚举类型 外,TS 是不会侵入 JS 的运行时,因此类型会被完全移除,
// 可以看一下最后编译出来的 JS 内容
type GConstructor<T = {}> = new (...arg: any[]) => T


// 三种需要混入的属性

type Positionable = GConstructor<{ setPosition: (x:number, y:number) => void }>;

// 类也可以用作类型,那么当然可以当做泛型
type Spritable = GConstructor<Sprite>

type Loggable = GConstructor<{ print: () => void }>

// 挑选第一个混入
function Jumpable<TBase extends Positionable>(Base: TBase){
    return class Jumpable extends Base{
        Jump(x:number, y:number){
            // 这里就有类型提示了
            // 开发者可以明确地知道需要用到的 Base 类的特定属性
            this.setPosition(x, y)
        }
    }
}

可选择的模式 Alternative Pattern

上述案例都是单个混入。那么混入多个对象呢?

下述案例直接在运行时往原型上混入对象,以确保类型系统与运行时一致。

class Jumpable{
    Jump(){}
}

class Duckable{
    ducn(){}
}

class Sprite{
    x = 0;
    y = 0;
}

interface Sprite extends Jumpable, Duckable {}
applyMixin(Sprite, [Jumpable, Duckable])

// 
function applyMixin(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)
            )    
        })
    })
}

限制

不能使用装饰器完成混入

const Pausable = (target: typeof Player) => {
    return calss Pausable extends target{
        shouldFreeze = false
    }
}

@Pausable
class Player{
    x = 0;
    y = 0
}

const player = new Player()

// carsh
// The Player class does not have the decorator's type merged:
player.shouldFreeze

补救方法

type FreezablePlayer = Player & { shouldFreeze: boolean };

const playerTwo = (new Player() as unknown) as FreezablePlayer;

playerTwo.shouldFreeze;

静态属性的混入

下面写法中 Gen 的 泛型 T  与 base() 函数中的泛型 T 的作用域不同,因此会导致出错。

具体情况如下:

function base<T>() {    return class Base {        static prop: T;    }}
// crash
// Base class expressions cannot reference class type parameters.
// 即,不能使用泛型 混入 静态类型
class Gen<T> extends base<T>() {}class Spec extends Gen<string> {}
// JSX 写法
<string>Spec.prop;

绕过上述机制的方法:

利用函数,使得 泛型的作用域保持一致

function base<T>() {  
    return class Base {    
        static prop: T;  
    }
}

function derived<T>() {  
    return class Derived extends base<T>() {    
        static anotherProp: T;  
    }
}

class Spec extends derived<string>() {}
class AnotherSpec extends derived<number>() {}

Spec.prop; // string

Spec.anotherProp; // string
AnotherSpec.prop; // number
Spec.anotherProp; // number