问题
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