为什么TypeScript无法识别混合对象之间共享的属性?

问题描述

我正在尝试在TypeScript中实现 mixins

这是代码

type Mixable = new (...args: any[]) => {};

class Base {
  constructor(protected _a: string,protected _b: string) {}
}

const A = (Base: Mixable) => class A extends Base {
  a() {
    console.log(this._a);
    return this;
  }
};

const B = (Base: Mixable) => class B extends Base {
  b() {
    console.log(this._b);
    return this;
  }
};

const C = A(B(Base));

const o = new C("a","b");

o.a().b();

但是不幸的是,编译器无法识别混合对象,链方法和发出的错误之间共享的属性

Here's the playground link

P.S。如果运行此命令,它将成功运行,并生成预期的输出,而不会出现任何JS错误

解决方法

为了访问属性_a_b,您需要强制将要应用mixin的类具有这些属性。

mixins在具有protected属性的打字稿中也可能很棘手,所以我将它们设为public

interface HasAB {
  _a: string;
  _b: string;
}

type Mixable = new (...args: any[]) => HasAB

class Base {
  constructor(public _a: string,public _b: string) {}
}

Typescript Playground Link

这可以清除访问属性的问题。

编辑:

不幸的是,我一直在玩这个游戏,只是不能与非公共财产一起很好地工作。使用this setup,组成的类能够访问传入类的受保护属性,但是它们给我们带来错误TS4094“导出的类表达式的属性'_a'可能不是私有的或受保护的。”因为mixin的属性必须是公开的。

我没有足够的信心说这不可能直截了当,但是如果我是你,我会考虑采用另一种设计模式。您可以将访问器方法getA()getB()添加到类Base中。或者,您可以定义readonly public属性ab,以访问基础private属性_a_b

这是第二种情况的分解方式:

我们定义一个interface AB,它具有public个属性ab。回想一下,打字稿界面中的所有属性本来就是public

interface AB {
  a: string;
  b: string;
}

我们声明如果可以用关键字Mixable创建某事物并且该创建的对象具有newa,则认为该事物b

type Mixable = new (...args: any[]) => AB;

我们更改Base使其实现interface AB,这将使其变为Mixable。您可以显式编写class Base implements AB,但不必这样做。只要它可以读取属性Mixablea,它就可以分配给b。我们使用javascript getter将ab实现为readonly属性,这些属性将读取_a_b的私有值,但不能对其进行修改。

class Base {
  constructor( protected _a: string,protected _b: string) {}

  get a(): string {
    return this._a;
  }

  get b(): string {
    return this._b;
  }
}

现在是我们的mixins。我们说要混合的类必须是Mixable类型。我们使用通用的<T extends Mixable>(Mixed: T),以便我们知道返回的类将具有传入类的所有属性和方法,而不仅仅是Mixable中的那些。这样可以解决链接问题。

请注意,我在示例中重命名了方法,因为我使用了ab作为属性的名称,所以我也不能将它们用作方法的名称。

const A = <T extends Mixable>(Mixed: T) => class A extends Mixed {
  aMethod() {
    console.log(this.a);
    return this;
  }
};

const B = <T extends Mixable>(Mixed: T) => class B extends Mixed {
  bMethod() {
    console.log(this.b);
    return this;
  }
};

现在您不再对此有任何疑问:

const C = A(B(Base)); // fine because Base extends Mixable

const o = new C("a","b");

o.aMethod().bMethod(); // fine because o.aMethod() returns an object with all of the abilities of C.

Typescript Playground Link

,

谢谢大家的贡献。它为这个问题增加了巨大的价值。

在进一步介绍之前,让我先解释一下我的初衷以及代码中的误解。

使用mixins的主要目的是分解大型类。由于这将应用于不同的类,因此我正在创建一个帮助器模块。为了使示例易于理解,我发布了一个更简单的实现。

关于Base标识符,我并不是说Base参数是指基类,而是父类(即扩展名的基类)。无论如何,为了摆脱混乱,我将改用标识符Parent

因此,我想出了一些我认为可以扩展的内容。请忽略怪异的名称Libbable(这只是为了说明这一点)。


type Mixable<T = {}> = new (...args: any[]) => T;

interface Libbable {
  x: string;
  a(): any;
}

type Mixin = Mixable<Libbable>;

class Lib {
  constructor(public x: string) {}
  a(): this { return this }
}

const A = <T extends Mixin>(Parent: T) => class extends Parent {
  a() {
    console.log(this.x);
    return this;
  }
};

const B = <T extends Mixin>(Parent: T) => class extends Parent {
  b() {
    return this.a();
  }
};

const L = A(B(Lib));
const o = new L("x");

o.a().b();

Link to the Playground


由于the design issue当前,TypeScript无法在混合对象之间共享非公共属性。我真的很希望能够使用此功能,否则,强大的访问修饰符在这里根本没用。

由于必须在Base类中声明方法才能在mixins中使用它们,因此一旦调用mixins的适用类方法,它们就会被调用。这不是我想要发生的事情。 JS允许我们摆脱这种情况,但TS不允许。如果您有更好的主意实现我想要的目标,请随时分享。