typescript

Why does contextual typing not work in a union of `fn | curried(fn)`?


Why does a union of these two types not provide parameter hints inside the functions?

(ts playground)

type Valish = (value: string) => boolean
type CurryValish = (...args: number[]) => Valish

const check = {
  vin: (value) => true,            // ✗ any, should be string
  cin: (curry) => (value) => true, // ✗ any any, should ne number string
} satisfies {
  [key: string]: 
    | Valish
    | CurryValish
}

const checkValish = {
  vin: (value) => true,            // 🗸 string => boolean
} satisfies { [key: string]: Valish }

const checkCurry = {
  cin: (curry) => (value) => true, // 🗸 number => string => boolean
} satisfies { [key: string]: CurryValish }

It seem to understand the context well enough to provide heplful information, even deep inside the CurryValish type. So, when replaced with valid types, why does it not provide the argument types?

const x: Valish | CurryValish = (a) => "hi"         
// Type 'string' is not assignable to type 'boolean | Valish'.(2322)

const y: Valish | CurryValish = (b) => (a) => "hi"
// Type '(a: any) => string' is not assignable to type 'boolean | Valish'.
//   Type '(a: any) => string' is not assignable to type 'Valish'.
//     Type 'string' is not assignable to type 'boolean'.

Solution

  • This is a design limitation of TypeScript, as described in microsoft/TypeScript#51639. According to a comment by the language's architect:

    We only infer contextual parameter types from a union of contextual function types when the contextual function types have identical parameter lists, so this is effectively a design limitation.

    Since Valish and CurryValish don't have identical parameter lists, there is no contextual typing happening.


    I imagine you were hoping that the compiler could examine the return type of the function and use that to discriminate the union. But this doesn't happen, especially because in general the return type of a function depends on its parameter types, and so the compiler tends to defer such inference until after it knows the parameter type. That's a circularity, and while it's possible to be smart and notice that x => true returns boolean no matter what type x is, this isn't always done. That general issue is described at microsoft/TypeScript#47599 (specifically in the context of inferring generic type parameters instead of discriminating unions) and it's a hard problem in general.

    So even if TypeScript allowed unions of functions with differing parameter lists to have contextual parameter types, you might not see exactly the behavior you want. The rest of the quoted comment above is:

    We could possibly do better by inferring the widest possible parameter types in such situations.

    which wouldn't serve to discriminate the union. But we won't know for sure until and unless unions of functions ever do get contextual parameter types. So I'll stop speculating here.