Issue
I have the following simplified code that fails to infer Set<number>
for the inner
property:
function test<S>(state: S, fn: (value: { outer: Set<S> }) => void) {}
function numberFn(value: { outer: Set<{ inner: Set<number> }> }) {}
test({ inner: new Set() }, numberFn)
// ^____
// Type 'Set<unknown>' is not assignable to type 'Set<number>'.
// Type 'unknown' is not assignable to type 'number'.ts(2322)
In my real code I have many calls like test
and with different types for the generic type parameter of Set
, and at the moment those types are not exported, so it would be a pain if I had to explicitly type them. I had hoped TypeScript could infer them, but I have been unable to get it to do so.
I wrote some tests to further my understanding and see if I could get to infer the right type.
function test1<T>(input: { arr: T[] }, value: { val: T }) {}
test1({ arr: [] }, { val: 5 })
// function test1<number>(input: {
// arr: number[];
// }, value: {
// val: number;
// }): void
That works. However this doesn't:
function test2<T>(input: { arr: Array<T> }, value: { val: T }) {}
test2({ arr: new Array() }, { val: 5 })
// function test2<any>(input: {
// arr: any[];
// }, value: {
// val: any;
// }): void
Probably to do with []
having special support over generic types in general. Confusingly enough when I try it with Set
rather than Array
and reverse the argument order, it does infer the right type:
function test3<T>(value: { val: T }, input: { arr: Set<T> }) {}
test3({ val: 5 }, { arr: new Set() })
// function test3<number>(value: {
// val: number;
// }, input: {
// arr: Set<number>;
// }): void
I can workaround it by do the inferring it for TypeScript explicitly, but this gets ugly, like so:
function test<S>(state: S, fn: (value: { outer: Set<S> }) => void) {}
function numberFn(value: { outer: Set<{ inner: Set<number> }> }) {}
test(
{ inner: new Set() } as Parameters<typeof numberFn>[0] extends { outer: Set<infer S> } ? S : never,
numberFn,
)
Can I rewrite my code such that TypeScript can infer the right type for state, like it does in test3
(the one using Set
with reversed arguments)?
Solution
In general, TypeScript cannot reliably infer both generic type arguments and the contextual type simultaneously. In your call to test()
, you need the type argument for S
to be inferred from numberFn
, which then implies that the argument {inner: new Set()}
should be checked against {inner: Set<number>}
, meaning that new Set()
should be interpreted in the context of Set<number>
, meaning that the type argument for the Set
constructor should be inferred as number
. But unfortunately the inference algorithm doesn't do things in this order. Somewhere along the way the compiler just gives up on contextual typing for state
, meaning that inference of the Set
constructor's type argument fails, and it falls back to unknown
. Set<unknown>
is not assignable to Set<number>
(it has some contravariant methods, as described in Difference between Variance, Covariance, Contravariance, Bivariance and Invariance in TypeScript) and it explodes.
function test<S>(state: S, fn: (value: { outer: Set<S> }) => void) { return state; }
function numberFn(value: { outer: Set<{ inner: Set<number> }> }) { }
test({ inner: new Set() }, numberFn) // error
// ~~~~~
// 'Set<unknown>' is not assignable to 'Set<number>'
This problem of simultaneous generic and contextual inference is a general limitation of TypeScript. The most relevant open issue about this is probably microsoft/TypeScript#47599. There has been steady improvement in support for this over the years, such as microsoft/TypeScript#48538, but there will unfortunately always be unsupported cases.
By far the easiest approach is for you to work around it and use Set<any>
instead of Set<unknown>
, since any
is intentionally looser than unknown
. Note that I modified test()
to return state
so you can see that S
is still inferred as {inner: Set<number>}
, and therefore the returned value is of type Set<number>
(and not as Set<any>
):
const inner = test({ inner: new Set<any>() }, numberFn).inner; // okay
// ^? const inner: Set<number>;
If you care more about inference than that, the only way I can think of which gives you the requisite control is to break apart test()
into pieces by currying it:
function curryTest<S>(fn: (value: { outer: Set<S> }) => void) {
return (state: S) => {
return state;
}
}
Now curryTest()
only accepts fn
and that will infer S
. It returns a new function which only accepts state
, and at that point, S
is set in stone. There is no simultaneous inference happening here:
const inner2 = curryTest(numberFn)({ inner: new Set() }).inner; // okay
// ^? const inner2: Set<number>;
You might be able to get similar behavior in a non-curried function by reordering arguments, as certain aspects of the inference algorithm work from left to right in the argument list. But if not, this is always an option.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.