My question is based on this question and answer
Let's say we have next code:
const myFn = <T,>(p: {
a: (n: number) => T,
b: (o: T) => void,
}) => {
// ...
}
myFn({
a: () => ({ n: 0 }), // Parameter of a is ignored
b: o => { o.n }, // Works!
})
myFn({
a: i => ({ n: 0 }), // Parameter i is used
b: o => { o.n }, // Error at o: Object is of type 'unknown'.ts(2571)
})
I was able to fix it. It works with extra generic:
const myFn = <T,>(p: {
a: (n: number) => T,
b: <U extends T /* EXTRA U generic */>(o: U) => void,
}) => {
// ...
}
I have fixed it intuitively. I can't explain why this error occured, and even more why my solution helped :)
I believe the answer is in Covariant/Contrvariant/Invariant/Bivariant definitions.
Could you please explain me why this error occured and why my solution works?
Thanks
UPDATE
It seems that this behavior is fixed/updated in TypeScript 4.7
Contextual type: The type, a code position must have based on the surrounding type it is assigned to.
Context-sensitive function: function expression with untyped parameters. The parameter type is derived from the contextual type.
type Fn = (s: string) => number
const fn : Fn = s => s.length
// `s => s.length` is context-sensitive function
// contextual type of `s` is `string`, obtained from `Fn`
The compiler does type inference in two phases:
first it infers all non-context-sensitive functions.
In the second phase inference results for type parameters (like T) are then used to infer context-sensitive functions.
If there are no inference candidates from phase 1 to be used in 2, you will probably get a type problem - in question case that is T getting type unknown.
Case 1:
myFn({
a: () => ({ n: 0 }),
b: o => { o.n }
})
b is context-sensitive, a not (parameter-less function). So, in the first phase we can analyze a and infer T to be { n: number }, which then can be used to type parameter o in b for phase 2. All fine here.
Case 2:
myFn({
a: i => ({ n: 0 }), // Parameter i is used
b: o => { o.n }, // Error at o: Object is of type 'unknown'.ts(2571)
})
Here is the devil. Both a and b are context-sensitive, so we need to skip phase 1 - no inference candidates for T. Both functions are now analyzed independently from each other in phase 2. This is a problem for the parameter type in b, as we cannot consult a anymore, what inferred type T has.
The only way is to infer unknown from the base constraint - and worse: the contextual type of T gets "fixed", as the internal TypeScript algorithm for phase 2 needs to have a concrete type instantiation at this point for T due to certain restrictions/optimizations. So we are irreversibly stuck with unknown for T.
Case 3:
const myFn = <T,>(p: {
a: (n: number) => T,
b: <U extends T /* EXTRA U generic */>(o: U) => void,
}) => {
// ...
}
works, as you introduced a complete new type parameter U with new inferences being made, independent from T. This is a clever workaround, though I haven't tested, if there are any downsides with this approach.
In order to solve nasty edge cases with type inferences, just make sure that at least one function with type parameters is not context-sensitive, by typing the function parameter explicitly.
For example, it is sufficient to type n: number inside a:
myFn({
a: (n: number) => ({ n: 0 }), // n explicitly typed
b: o => { o.n }, // works again
})
Playground code
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