为什么 TypeScript 无法识别 mixin 之间共享的属性?

Why does TypeScript unable to identify the properties shared among mixins?

我正在尝试在 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();

但不幸的是,编译器无法识别 mixin、链方法和发出的错误之间共享的属性。

Here's the playground link.

P.S。如果你 运行 这个,它 运行 成功,并生成预期的输出,没有任何 JS 错误。

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

此外,mixin 在具有 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

这解决了您访问属性的问题。

编辑:

我一直在玩这个,不幸的是 mixins 只是不能很好地与 non-public 属性一起工作。使用 this setup 组合的 classes 能够访问 passed-in class 的受保护属性,但它们给我们错误 TS4094“属性 '_a' of exported class 表达式可能不是私有的或受保护的。”因为 mixin 的属性必须是 public.

我没有足够的信心说这是 straight-up 不可能的,但如果我是你,我会考虑不同的设计模式。您可以将访问器方法 getA()getB() 添加到您的 class Base。或者您可以定义 readonly public 属性 ab 来访问基础 private 属性 _a_b.

第二个案例的分解方式如下:

我们定义了一个 interface AB,它具有 public 属性 ab。回想一下,typescript 接口中的所有属性本质上都是 public.

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

我们声明如果可以使用关键字 new 创建并且创建的对象具有 ab.[=58,则该对象被视为 Mixable =]

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

我们改变 Base 使其实现 interface AB,这将使它成为 Mixable。您可以明确地写 class Base implements AB 但您不必这样做。只要它可以读取属性 ab,它就可以分配给 Mixable。我们使用 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;
  }
}

现在开始我们的混入。我们说要混合的class必须是Mixable类型。我们使用泛型 <T extends Mixable>(Mixed: T) 以便我们知道返回的 class 将具有 passed-in class 的所有属性和方法,而不仅仅是 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

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

在我更进一步之前,让我解释一下我的初衷和我的代码引起的误解。

我使用 mixin 的主要目标是分解我的大型 classes。因为这将应用于不同的 classes,所以我正在创建一个辅助模块。为了让示例易于理解,我发布了一个更简单的实现。

关于 Base 标识符,我并不是说 Base 参数指的是基 class,而是父(即扩展的基)。无论如何,为了消除混淆,我将使用标识符 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 无法在 mixin 之间共享 non-public 属性。很期待能用到这个,不然再强大的访问修饰符在这里根本派不上用场

因为必须在 Base class 中声明方法才能在混入中使用它们,所以一旦调用混入的适用 class 方法,就会调用它们。这不是我想要发生的。 JS 可以让我们去掉这个,但是 TS 不行。如果您有更好的想法来实现我想要的,请随时分享。