Description
Apologies in advance, as this might be more of a question, but I'm asking here rather than StackOverflow because there might be some changes to the type checking that could allow this pattern to be easier to express, and I might be encountering some bugs.
I'm trying to convert the subclass factory mixin pattern to TS, with enough type info to be able to determine the complete object shape of a class that uses these mixins. With intersection types and f-bounded quantification, it seems like we should be in really good shape, but I ran into several difficulties and maybe some bugs.
TypeScript Version:
1.8.0
Code
Here's the untyped JS:
let M1 = (superclass) => class extends superclass {
foo() { return 'a string'; }
}
let M2 = (superclass) => class extends superclass {
bar() { return 42; }
}
class C = M2(M1(Object)) {
baz() { return true; }
}
let c = new C();
console.log(c.foo(), c.bar(), c.baz());
When converting to TypeScript, the first issue is that class extends superclass
complains that superclass
is not a constructor function type, so I introduce an interface for that:
interface Constructable {
new (): Object;
}
let M1 = (superclass: Constructable) => class extends superclass {
foo() { return 'a string'; }
}
let M2 = (superclass: Constructable) => class extends superclass {
bar() { return 42; }
}
This causes the mixin declarations to pass type checking, but the usage loses type information for nested mixins:
console.log(c.foo(), c.bar(), c.baz()); // Property 'foo' does not exist on type 'C1'.
With intersection types, I hope to be able to type the subclass factories such that they include both the superclass and class they declare. Something like:
let M1 = <T>(superclass: Constructable<T>): T & M1 => class extends superclass {
foo() { return 'a string'; }
}
Obviously I can't reference M1
like this because it refers to the class factory, not the type that the factory returns. I can remove it though:
let M1 = <T>(superclass: Constructable<T>): T => class extends superclass {
foo: string;
}
and now I get the error: TS2322: Type 'typeof (Anonymous class)' is not assignable to type 'T'.
Which makes sense, because superclass is a Constructable<T>
, not a T
, which should be solvable by the new support for f-bounded quantification:
interface Constructable<T extends Constructable<T>> {
new (): T;
}
let M1 = <T extends Constructable<T>>(superclass: T): T => class extends superclass {
foo: string;
}
But now I get another error on extends superclass
: TS2507: Type 'T' is not a constructor function type. even though T
should implement Constructable<T>
which is a constructor function type. Is this a bug?
Another possible bug I ran into is with my attempt at the Constructable
interface. As I mentioned, I can get the declaration to (questionably) pass type checking, before using recursive constraints:
interface Constructable<T> {
new (): Object;
}
let M1 = <T>(superclass: Constructable<T>) => class extends superclass {
foo() { return 'a string'; }
}
but the declaration for new
is off, it should be:
interface Constructable<T> {
new (): T;
}
But this triggers the error: TS2509: Base constructor return type 'T' is not a class or interface type.
neither of these variants fix it:
interface Constructable<T extends Object> {
new (): T;
}
or:
interface Constructable<T> {
new (): T & Object;
}
Even once these issues (which might be my fault, I hope!) are over come, there's another problem of being able to refer to the type returned by a subclass factory. It seems like I would have to define an interface as well as the class expression, which is enough duplicate work to make this pattern very cumbersome to use in TypeScript.
Assuming tsc can eventually correctly infer the type returned by a subclass factory M1
, it would be great to be able to refer to that type for use in implements
, etc.
Activity
DanielRosenwasser commentedon Feb 25, 2016
I think #4890 is related.
A few notes since it looks like you might be slightly new to TS:
This is unfortunately a common mistake.
Object
is never actually what you want. You either wantany
in this instance. Alternatively, you probably wantwhich you've done below.
I think this signature is slightly misguided. This is saying you're passing in a constructor whose constructed type is identical to the type of the constructor. Something like:
But then you could do this:
So that's probably not what you'd want.
justinfagnani commentedon Feb 25, 2016
Good points. And I am fairly new to TS. I seem to have mixed up the static and instance side interfaces.
#4890 looks like nearly exactly I'm trying to do. I might be able to close this.