2

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

1 Answers1

8

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:

  • 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.

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

A_blop
  • 792
  • 2
  • 11
  • 2
    Note: this is to the best of my knowledge. Information is spread sparingly among github issues and blogs, so please free to comment/criticize if you don't agree with any of the points. – A_blop Jan 03 '21 at 19:01
  • 1
    Thank You very much. It is very good answer. You have a really deep knowledge of TS compiler! – captain-yossarian from Ukraine Jan 03 '21 at 22:04