typescript tells me this is assignable to type T, but T could be instantiated with different subtype of constraint MainModel
class HelperClass {
public static someHelp<T extends MainModel<T>>(key: keyof T, instance: T): T[keyof T] {
return instance[key];
}
}
abstract class MainModel<T extends MainModel<T>> {
someProperty: string = "";
someMethod(key: keyof T) {
// some code
return HelperClass.someHelp(key, this); // <- error here
}
}
class FirstModel extends MainModel<FirstModel> {}
const firstModel = new FirstModel();
firstModel.someMethod("someProperty");
I don't understand the error message, am I doing something wrong or my code is not complete?
What does it mean "could be instantiated with a different subtype" if my subtype is generic?
You are using a recursively bounded generic in class MainModel<T extends MainModel<T>>
, presumably so that the generic type parameter T
can be used as a synonym for the type of this
inside the class. Unfortunately, it's not a synonym, it's a constraint. That is, T
might not be identical to MainModel<T>
; all you know is that T
is some subtype of MainModel<T>
. That leads to possibilities like
class FirstModel extends MainModel<FirstModel> {
x = 1;
}
class Oops extends MainModel<FirstModel> { }
new Oops().someMethod("x").valueOf(); //allowed to compile, but runtime error 💥
where FirstModel
is actually more specific than MainModel<FirstModel>
(as it has an extra x
property), and thus you can write a class Oops
that extends MainModel<FirstModel>
without extending FirstModel
. And so an Oops
has a someMethod()
that accepts "x"
as an input, and the implementation of someMethod()
will try to access the x
property of this
, and you get undefined
, not T[keyof T]
. Oops.
This might not be likely to occur, but TypeScript doesn't realize that, so it complains. If you're confident that this won't be a problem, you can always assert that:
someMethod(key: keyof T) {
return HelperClass.someHelp(key, this as MainModel<T> as T);
}
where the type assertion is how you acknowledge the possibility of the implementation being unsafe.
In some other languages, notably Java and (at least historically) C++, recursively bounded generics (known as F-bounded polymorphism or the Curiously Recurring Template Patern) was the closest you could get to capturing the type of this
.
But in TypeScript, that use case is better served by using the polymorphic this
type. The type named this
is automatically the type of this
, such that potential subclasses will have more specific this
types. It's implicitly generic, and constrained to the current class, so it's very similar to class F<this extends F<this>> {}
with a type parameter named this
, but you have no opportunity outside the class to specify that with a weird type argument like FirstModel
for Oops
.
If I switch to using polymorphic this
, your problem goes away (and the code looks simpler since the generic is now implied and you don't need to carry it around everywhere):
abstract class MainModel {
someProperty: string = "";
someMethod(key: keyof this) {
return HelperClass.someHelp(key, this); // okay
}
}
class HelperClass {
public static someHelp<T extends MainModel>(
key: keyof T, instance: T): T[keyof T] {
return instance[key];
}
}
And if you try to reproduce the failure mode, you'll find it's not possible:
class FirstModel extends MainModel {
x = 1;
}
class Oops extends MainModel { }
new Oops().someMethod("x").valueOf(); // <-- now this is a compiler error
There's just no explicit type argument you need to "get right" anymore, it's automatically set to that of the current class.