Why does this TypeScript mixin employing generics fail to compile?

南笙酒味 提交于 2021-01-29 04:04:20

问题


I'm using mixins/traits with TypeScript using a subclass factory pattern as described at https://mariusschulz.com/blog/mixin-classes-in-typescript. The trait in question is called Identifiable, which imparts an id property to a class that should express the Identifiable trait. When I attempt to use the trait with another, non-generic trait (Nameable) in a certain order, compilation fails.

class Empty {}

type ctor<T = Empty> = new(...args: any[]) => T;

function Nameable<T extends ctor = ctor<Empty>>(superclass: T = Empty as T) {
  return class extends superclass {
    public name?: string;
  };
}

function Identifiable<ID, T extends ctor = ctor<Empty>>(superclass: T = Empty as T) {
  return class extends superclass {
    public id?: ID;
  };
}

class Person1 extends Nameable(Identifiable<string>()) { // compiles
  constructor(name?: string) {
    super();
    this.name = name;
    this.id = "none";
  }
}

class Person2 extends Identifiable<string>(Nameable()) { // fails to compile
  constructor(name?: string) {
    super();
    this.name = name;
    this.id = "none";
  }
}

Compilation error is

src/test/unit/single.ts:30:10 - error TS2339: Property 'name' does not exist on type 'Person2'.

30     this.name = name;
            ~~~~

How do I get generic traits to compile correctly, regardless of the order in which they're used?

NB: public git repo for this question is at https://github.com/matthewadams/typetrait. If you want to play with this, make sure to checkout the minimal branch.


回答1:


The issue is very simple actually, and it is related to the fact that typescript does not have partial type argument inference. The call Identifiable<string>(...) does not mean you set ID and let the compiler infer T. It actually means use string for ID and use the default (ie Empty) for T. This is unfortunate and there is a proposal to allow partial inference but it hasn't gained much traction.

You have two options, either use function currying to do a two call approach, where the first call passes ID and the second call infers T:

class Empty { }

type ctor<T = Empty> = new (...args: any[]) => T;

function Nameable<T extends ctor = ctor<Empty>>(superclass: T = Empty as T) {
  return class extends superclass {
    public name?: string;
  };
}

function Identifiable<ID>() {
  return function <T extends ctor = ctor<Empty>>(superclass: T = Empty as T) {
    return class extends superclass {
      public id?: ID;
    };
  }
}


class Person2 extends Identifiable<string>()(Nameable()) {
  constructor(name?: string) {
    super();
    this.name = name;
    this.id = "none";
  }
}

Playground link

Or use inference on ID as well by using a dummy parameter as an inference site:

class Empty { }

type ctor<T = Empty> = new (...args: any[]) => T;

function Nameable<T extends ctor = ctor<Empty>>(superclass: T = Empty as T) {
  return class extends superclass {
    public name?: string;
  };
}

function Identifiable<ID, T extends ctor = ctor<Empty>>(type: ID, superclass: T = Empty as T) {
    return class extends superclass {
      public id?: ID;
    };
  }
}


class Person2 extends Identifiable(null! as string, Nameable()) {
  constructor(name?: string) {
    super();
    this.name = name;
    this.id = "none";
  }
}

Playground link



来源:https://stackoverflow.com/questions/57347557/why-does-this-typescript-mixin-employing-generics-fail-to-compile

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!