This is best explained using an example. I need to reference the current class in a static method, this works as expected:
class Cls {
static fn<T extends typeof Cls>(
this: T,
arg: T extends typeof Cls ? true : false,
) {}
}
Cls.fn(true);
Cls.fn(false); // Argument of type 'false' is not assignable to parameter of type 'true'.
However, when calling Cls.fn from another static method, it doesn't work:
class Cls {
static fn1<T extends typeof Cls>(
this: T,
arg: T extends typeof Cls ? true : false,
) {}
static fn2<T extends typeof Cls>(this: T) {
this.fn1(true); // Argument of type 'true' is not assignable to parameter of type 'T extends typeof Cls ? true : false'.
}
}
TS Playground
Interestingly, it works if I remove fn2's generic or do this.fn1<typeof Cls>(true). This means the error is related to the type of this. I think it has to do with this referring to any subclass of Cls as opposed to exactly Cls. However, even if this is a subclass of Cls, T extends typeof Cls would still be true.
Is this a bug where Typescript uses the wrong value for this? If it's not a bug, how can I fix it?
In my actual code, I need to reference the current class because the method accepts different arguments depending on the subclass.
Edit: here's a more realistic example using subclasses
The behavior is explained by an unresolved generic type parameter.
This is caused by an indirection introduced by what is basically a higher-order function (method). In the case of a non-generic method fn2 (using the Sub/Base example), the inference works as expected as the type parameter gets resolved to typeof Sub in the derived class Sub:
Base.fn<typeof Sub>(this: typeof Sub, arg: "sub"): void
Unfortunately, in the case of fn3, it is itself a generic function (method), leading to the T generic parameter in calls to fn being unresolved, which can be seen from the inferred signature:
Base.fn<T>(this: T, arg: T extends typeof Sub ? "sub" : "base"): void
This clears up what the following compiler error means - as the conditional type is also left unresolved, neither "sub" nor "base" will be assignable to T extends typeof Sub ? "sub" : "base":
Argument of type '"sub"' is not assignable to parameter of type 'T extends typeof Sub ? "sub" : "base"'
One could, as you rightfully noted, remove the generic type parameter of the higher-order method, thus removing the indirection:
static fn3(this: typeof Sub) {
this.fn('base'); // error
this.fn('sub'); // OK
}
This, however, presents complications in further derived classes should you ever need this:
class Sub {
static fn4(this: typeof Sub) {
this.fn('sub');
return this;
}
}
class SubSub extends Sub {}
SubSub.fn4(); // typeof Sub, probably wanted typeof SubSub
There is an alternative, though - do not constrain the T parameter and instead constrain the type of this based on what T is inferred to be. The classic technique is using a conditional type that resolves to itself of never:
class Sub extends Base {
static fn3<T>(this: T extends typeof Sub? T: never) {
this.fn('base'); // error
this.fn('sub'); // OK
return this;
}
}
Sub.fn('sub'); // OK
Sub.fn('base'); // error
Sub.fn3(); // typeof Sub
The signature of the inner this.fn() call is inferred as:
Base.fn<T extends typeof Sub ? T : never>(this: T extends typeof Sub ? T : never, arg: (T extends typeof Sub ? T : never) extends typeof Sub ? "sub" : "base"): void
By further deferring the evaluation via T extends typeof Sub ? T : never we actually helped the compiler: now it nows that T is guaranteed to be typeof Sub by the time of instantiation.
Playground
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With