Used terms
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.
Example
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`
How type inference works
The compiler does type inference in two phases:
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
.
Carried over to your cases
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.
Alternative solution
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