Reputation: 4144
Trying to understand typescript generics + static class functions a bit more thoroughly, and I am running into to some type problems:
class Base {
value : string = 'default'
static identity<M extends typeof Base>() : M {
return this // Type Error: Type 'typeof Base' is not assignable to type 'M'
}
}
class Extended extends Base {
otherValue = 'extended'
constructor() {
super()
}
}
let baseClass = Base.identity()
let extendedClass = Extended.identity()
expect(new baseClass().value).to.eq('default')
// Type Error: Property 'otherValue' does not exist on type 'Base'
// How do I make Extended.identity() be `typeof Extended` without
// an explicit cast?
expect(new extendedClass().otherValue).to.eq('extended')
If I ignore type errors and run the output code, everything runs as expected and expectations are met. I think this is probably due to some understanding gaps with static functions, but any help in understanding the problem would be much appreciated.
Upvotes: 1
Views: 2948
Reputation: 21
Extends @jcalz 's answer to support instance type (works on [email protected]):
class A {
static a<T extends typeof A>(this: T) {
return new this();
}
static b<T extends typeof A, I = InstanceType<T>>(this: T): I {
return (new this()) as I;
}
}
class B extends A {}
let a = A.a(); // type: A
let b = B.a(); // type: A
let a2 = A.b(); // type: A
let b2 = B.b(); // type: B
Upvotes: 0
Reputation: 328262
It looks like what you really want is polymorphic this
on static methods, which TypeScript doesn't directly support. What it does seem to support (and it's mentioned in that Github issue), is using this
parameters on the method:
class Base {
value : string = 'default'
// specify that this is M
static identity<M extends typeof Base>(this: M) : M {
return this;
}
}
class Extended extends Base {
otherValue = 'extended'
constructor() {
super()
}
}
The static identity()
method will only work if the object on which you call it is assignable to a subtype of the Base
constructor type. The compiler apparently will use type inference to determine the right value for M
when you call it. You can verify that things behave as expected:
let baseClass = Base.identity() // Base.identity<typeof Base>
let extendedClass = Extended.identity() // Base.identity<typeof Extended>
It's not well documented, and it frankly surprised me that you can actually get it to work. If you really want polymorphic this
in static methods to work without the above voodoo, you might want to go to Microsoft/TypeScript#5863 and give the issue a 👍 or describe your use-case if you think it's particularly compelling.
Hope that helps; good luck!
Upvotes: 2
Reputation: 1135
Your problem is that M is more concrete than Base, and you are trying to assign Base to M. Imagine this:
Base
Base
-> Base+A
(A indicates additional properties that the less generic class has)You invoke Base.identity
method on Base+A
class:
identity<Base+A extends typeof Base>(): Base+A {
return this; // This is a Base class method, so it's typeof Base
}
Compiler does not find the A part in the return value's type, so it's throwing an error.
Upvotes: 1