Given the following code, why does Typescript error in the getInferred
method? Is there a case where ValueOf<this>
and T
could be different?
interface Wrapper<T> {
value: T;
}
type ValueOf<T> = T extends Wrapper<infer U> ? U : never;
class Foo<T> implements Wrapper<T> {
value: T;
constructor(value: T) {
this.value = value;
}
getInferred = (): ValueOf<this> => {
// Type 'T' is not assignable to type 'GetGeneric<this>'.
return this.value;
}
getSimple = (): T => {
// Works Fine
return this.value;
}
}
For my use-case, I'm adding methods dynamically to a class and ValueOf<this>
provides better return types for the dynamic methods.
const mixin = {
getFooInferred<Self extends Foo<any>>(this: Self) {
return this.getInferred();
},
getFooSimple<Self extends Foo<any>>(this: Self) {
return this.getSimple();
}
}
function makeFooWithMixin<T>(value: T) {
const foo = new Foo(value);
Object.defineProperties(foo, {
getFooInferred: {
value: mixin.getFooInferred,
},
getFooSimple: {
value: mixin.getFooSimple,
}
});
return foo as Foo<T> & typeof mixin;
}
const foo = makeFooWithMixin("hello")
// When using the returntype of `getInferred`, we correctly get `string` as the type here
const resultInferred = foo.getFooInferred()
// When using `getSimple`, we instead get `any` because `getFooSimple` types the `Self` generic as `Foo<any>`
const resultSimple = foo.getFooSimple();
Typescript playground link for all above code
CodePudding user response:
The polymorphic this
type is implemented as an implicit generic type parameter that all classes and interfaces have (see microsoft/TypeScript#4910). And your ValueOf<T>
type, defined as
type ValueOf<T> = T extends Wrapper<infer U> ? U : never;
is a conditional type. So ValueOf<this>
is a conditional type that depends on a generic type parameter.
And unfortunately, the TypeScript compiler is unable to do much reasoning about what values will be assignable to such a type. It defers evaluation of the type and can only know what it really is once this
is specified, such as in the call new Foo("x").getInferred()
, where this
will be Foo<string>
. Inside the body of getInferred()
, this
is unspecified (it can be any subtype of Foo<T>
), and so ValueOf<this>
is essentially opaque to the compiler. It isn't that this.value
can be of a type other than ValueOf<this>
, but rather that the compiler cannot see it. It will reject any value that is not already of type ValueOf<this>
.
If you use a type assertion like this.value as ValueOf<this>
, then the compiler will allow you to return that, but only because you are claiming that this.value
is of type ValueOf<this>
, and not because the compiler can tell one way or the other:
getInferred = (): ValueOf<this> => {
return this.value as ValueOf<this>; // okay
}
In general, if you need to provide a value of a generic conditional type, you'll have to do something unsafe like a type assertion. But in this particular instance, you have an alternative. All you're doing with ValueOf<T>
is looking up the value
-keyed property in T
. And this can be done without conditional types. You can use an indexed access type instead:
type ValueOf<T extends Wrapper<any>> = T['value']
And even though the compiler still isn't great at understanding arbitrary manipulations of generic types, it does know that if you have a value of type T
and a key of type K
that the property value you read at that key will be of type T[K]
, even if T
or K
are generic. So it should be able to verify that this.value
is of the type this["value"]
:
getInferred = (): ValueOf<this> => {
return this.value; // okay
}
And indeed it can.