Repository: TypeScript
Issue: Improvement/Bug in contextual inference where the call-site is generic.
Labels: Suggestion,In Discussion

# Feature request / Bug report

## 🔍 Search Terms

Contextual inference, generic call-site

## ✅ Viability Checklist

My suggestion meets these guidelines:

- [x]  This wouldn't be a breaking change in existing TypeScript/JavaScript code
- [x]  This wouldn't change the runtime behavior of existing JavaScript code
- [x]  This could be implemented without emitting different JS based on the types of the expressions
- [x]  This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- [x]  This feature would agree with the rest of [TypeScript's Design Goals](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals).

## ⭐ Suggestion / Problem

Let's first study contextual inference in different scenarios. [Playground](https://www.typescriptlang.org/play?jsx=0&ts=4.4.0-beta#code/C4TwDgpgBAGgPAFSgXigOwgNwgJwHwpQDeUAJAFxQBEAZgPZ1VQA+USAvgFAAmEAxgBsAhjmh86aAM7AyARkpwAWngAUAD0qKAlCgIkKURVC69BIsROlkATAuXrKAOToBJNDVxK8O5HrKbjTlBIKGc3DxxEAlQAbQQAXTioCDVgCDRuSSghNBAoAH4oAAYoSgxsHHjOTgEIGQB9ShgAbk56wlJZFSoqHSh+gcHBgHphqElZYFkoAFpDKABLLIX3XFFubKye7IyobjoILLQ6GXEAWzAF2qgAOjv7h8en+6g6AGs2jutu3qG-kbGE2A1lm8yWi1WOHWm2o9EYOw2+0O6BOUHOl2uzyxT3QCz4EE+qE63Thv3+APGkwAzKCjOCVhFoUItqSEWi6BcrsjsTyea8Pu0id9aAwyeT+qNKcAACy0xbLSFMlmitnorlZXmanFoPEEnj8YSidlSGRneRQBywHwETB0BbcThmlTEnpaPriimSaxTOX0xUQDbM6hMHKIg5HVFqzFavnvR1dUjC3ruj0SwHekFzOkKxkBmEi+GhvbhlGnDkY6Ax564-Hx51dAvJ1NQSVe4A0rPyiG5wPKwu7KPcqta-l1xMk0Vu5ut72yzt+nv51lFwca4eamt60yGiwmqBnWxQKKW8K4ADKwBwKwA5gAZBZpHBCARRAA0UEasDgYUhUW8uigW17SCcBoFPHALyvNA7wfXBnyiQgkBSNIMiyaQoOvAo2FKcZLxvR1hSEKh3xdJtxVbKkfXnHM1jzINtiLJEIzLTlo3XR5RwPboiJIpMp3IwFKMzMEaKhOi+yYVgqCItkmNLdlWMrGNNwI7jiLkCdGH4oYKPbX1RKVWEVRXct1VudjYw+LjpPU8cbO0v5dLnETu1o3sjPhKSZJMxS1wsu4VP1MwjXEPczipSgVE-EgNFgYxrUAu0HXClQYsoUidHYFNPWlKiXIZNz8xDXY5OOFiK3M-yArjFK0psH5MuygZW1y4Ts1csT3MbWSSzKhSKqqlTaqgWKXVJRqdMBXKO3y-0uuXAdTNqPyqo4mqqVSkb0uFcb4sm8Zcuc9qCs6pdjMW3zKtWh5Au3cxjSsM5pQUJDUnSTJiGyfIygAVzOAAjXB31i+A3ggEA6BoNgCHYVRPwQBKgOS6VNtGhtenfIRKGmLK03GABWPLjrmoqeuRPrByu-z+SgR0UbquyMeybG9pbQFCbarsTsM9CbxYdA-sBnB+ckEAAboAQyeY-quReH7rwAC04ZsVclf4VAARx+vE3gZOgubQcQxL4GRJAgaAAGJpWlAAOAB2ABOLQ6dR9KGyIrRMZZ3HW0JmbicXejvJK3rIyWpTBpq+mtvq+yvagHGdF9mV9I6nm8Og-m0EF3ARbF-6Jal+TKYC+WFfL5WVdTNW-k17W+F19x9b9I3RBN8ZzagK3bcd52gp3B7TXxl73wAVWSN7UK-BA8FULGoHAyCb3vR94Jn4HKFHxGksdfG1JI9GHMcwEADYiYNwOtmKsNybDxSqYsmnd-32PxqatnxjPzmF0KoMfrQN4xwADuaAi4U3DtTMulcq4CXJHXHWesDat34KbTu3d7ZO2fjZA+akj6ejPv7C+v8tjBxvtLEuq1OJ72wbHD2788aSDPkdIhp0-4AOAaAxiodypcgfiOMuFcYEehrkMeBDdEEtzoMbVBltrYYK0EAA) for code below. **Highly recommended** to view it in the playground so that you can see where are the red underlines, what is the inference and what are the completions.

```tsx
type X<T = never> = { $: "foo" | T }
declare const $1: <Z>(x: Z) => { $: Z }
declare const $2: <Z>(x: NoInfer<Z>) => { $: Z }
type NoInfer<T> = [T][T extends any ? 0 : never]

let _: X;
_ = $1("")               // s1t1 - Z is inferred as "" and does not compile ........................ ok
_ = $2("")               // s1t2 - Z is inferred as "foo" and does not compile ..................... nice
_ = $1("foo")            // s1t3 - Z is inferred as "foo" and compiles ............................. ok
_ = $2("foo")            // s1t4 - Z is inferred as "foo" and compiles ............................. nice

declare const m1: (x: X) => void
m1($1(""))               // s2t1 - Z is inferred as "" and does not compile ........................ ok
m1($2(""))               // s2t2 - Z is inferred as "foo" and does not compile ..................... nice
m1($1("foo"))            // s2t3 - Z is inferred as "foo" and compiles ............................. ok
m1($2("foo"))            // s2t4 - Z is inferred as "foo" and compiles ............................. nice

declare const m2: <T>(x: InferStringLiteral<T>, _: X<NoInfer<T>>) => void
type InferStringLiteral<T> = T extends string ? T : string
m2("a", $1(""))          // s3t1 - Z is inferred as "" and does not compile ........................ ok
m2("a", $2(""))          // s3t2 - Z is inferred as "foo" | "a" and does not compile ............... nice
m2("a", $1("foo"))       // s3t3 - Z is inferred as "foo" and compiles ............................. ok
m2("a", $2("a"))         // s3t4 - Z is inferred as "foo" | "a" and compiles ....................... nice

declare const m3: (_: { x: X }) => void
m3({ x: $1("") })        // s4t1 - Z is inferred as "" and does not compile ........................ ok
m3({ x: $2("") })        // s4t2 - Z is inferred as "foo" and does not compile ..................... nice
m3({ x: $1("foo") })     // s4t3 - Z is inferred as "foo" and compiles ............................. ok
m3({ x: $2("foo") })     // s4t4 - Z is inferred as "foo" and compiles ............................. nice

declare const m4: <T extends { a?: number, x: X<keyof T> }>(_: T) => void
m4({ x: $1(""), a: 1 })  // s5t1 - Z is inferred as "" and does not compile ........................ ok 
m4({ x: $2(""), a: 1 })  // s5t2 - Z is inferred as string | number | symbol and does not compile... ugh
                         //        (quickinfo is incorrect see #44879)
m4({ x: $1("a"), a: 1 }) // s5t3 - Z is inferred as "a" and does not compile ....................... ok
m4({ x: $2("a"), a: 1 }) // s5t4 - Z is inferred as string | number | symbol and does not compile .. ughhh
                         //        (quickinfo is incorrect see #44879)

declare const m5: <T, U extends X<T>>(a: InferStringLiteral<T>, x: U) => void
m5("a", $1(""))          // s6t1 - Z is inferred as "" and does not compile ........................ ok 
m5("a", $2("foo"))       // s6t2 - Z is inferred as unknown and does not compile.................... ugh
                         //        (quickinfo is incorrect see #44879)
m5("a", $1("a"))         // s6t3 - Z is inferred as "a" and does not compile ....................... ok
m5("a", $2("a"))         // s6t4 - Z is inferred as unknown and does not compile ................... ughhh
                         //        (quickinfo is incorrect see #44879)
```

1. The contextual inference is expected and consistent for s1, s2, s3 & s4. But weird yet consistent for s5 & s6.
2. I think the following should be the expected behavior...

    ```
    s5t2 - Z is inferred as "foo" | "a" | "x" and does not compile
    s5t4 - Z is inferred as "foo" | "a" | "x" and compiles
    s6t2 - Z is inferred as "foo" | "a" and does not compile
    s6t4 - Z is inferred as "foo" | "a" and compiles
    ```

3. In s5t4 & s6t4, `"a"` is one of the completions but they are incorrect because the language server infers the type parameter different from the compiler. This is most probably a bug which might be accommodated in [#44879](https://github.com/microsoft/TypeScript/issues/44879). So all in all the completions are useless and when the language server is fixed there would be no completions.
4. Generally speaking, a sugar-like abstraction should not result in compromises in developer experience. If you inline `$*("")`  as `{ $: "" }` in s5 & s6 then the completions and the compiler would work as expected.
5. My analysis is that when the type expected from calling  `$*` is generic itself (ie s5 & s6) then compiler can't infer the type parameter `Z` correctly. It resolves all other generics to `unknown` hence the type parameter `Z` is inferred as  `keyof unknown` and `unknown` in s5 and s6, respectively.<br/>
Aside: Though I would expected other generics to resolve to their constraint meaning I'd have expected (like not ultimately but considering the weirdness itself) the type parameter of `$2` in the following scenario as `"a" | "b"` instead of `unknown`. [Playground](https://www.typescriptlang.org/play?jsx=0&ts=4.4.0-beta#code/C4TwDgpgBAGgPAFSgXigOwgNwgJwHwpQDeUAJAFxRIA+UARAGYD2TdUAvgFAAmEAxgBsAhjmh8maAM7AoAWwBslRFAgAPYBDTdJ9IW1p0ARnQA0UAKor1m7bER48ACiGUEZ1ZXMBKFAUxMAS25OBUc6PTNSACYwvS8fAHoEqAAtKACdALQGXFFuKCEdAFc0AGs0JgB3NE4efmFRKHEpGWilFKcPKAA5JgBJbNw4Dp9kAhIKVI5OUEge-sGce0IAbQQAXTWrDS0dITQQKAB+KAAGKEoMbBx1oA).

    ```tsx
    type X<T = never> = { $: T | "foo" }
    declare const m6: <T extends "a" | "b", U extends X<T>>(a: T, x: U) => void
    m6("a", $2("a")) // Z is inferred as unknown

    declare const $2: <Z>(x: NoInfer<Z>) => { $: Z }
    type NoInfer<T> = [T][T extends any ? 0 : never]
    ```

6. [#44999](https://github.com/microsoft/TypeScript/issues/44999) is most probably the consequence of this weirdness as it fits the precondition and it's marked as a bug, so imo this should be also be considered as a bug. It's not apparent because the repros are vague but hopefully the following examples would make it more clear how essential it is to get this right.
7. Maybe `$1`should work same as `$2`

## 📃 Motivating Example

Library authors provide sugar-like abstractions all the time. Take the following as an example. [Playground](https://www.typescriptlang.org/play?jsx=0&ts=4.4.0-beta#code/MYJwpghgLmDCD2AbeIDOAKA3gKAAS4CMJUwAuXTXABxAEsBbCEAT3ICJwATN3AXwBo8uTmDoA3MJ3I58+GgybMALAAYV5RLQDmACxgA7AOq0oOgHLwAkvoBmo9G3mMWbfrhUA6JQEpBs6nTOzACsahraemBGJjrwAK5QFtZ2IA5Oiq7uHsG+QnKBigBsYbiaugbGpvGJVrb2jgUuuMS4wPD6qFBunoW5-uksAOwlZZHRVQlJdSCVOsmiAMpQdPpaADImohCIaY3MmZ6D3kK82LzH2CLAiExgre2dreDQcEgoqOQAPLC4YAAeBk4qFwnRWWgAfOhTGB6GQhJQiCRpLgANptZAgSwifRQWg2WiiXC0fS4WAAXXICAxfCEbhE4kk5DyskoKPptAknCpKCJJNBxK0FNwzP8+ARxDIuCmKW+4NwAHp5bgAAbS0TKkHwXCmaAq2Aa2jA4kpLi4dqIZi4GwgeD0FWIsDKkWitzwKhbXHtcjoYCUt4gby4AC8cu5IGd-lOopp+HOwblbAAKgB5AAiybY2EuYGut3uHSgpQiFRiapApCEnwAWpCHeQy9XwW4AO5gYvkfRxegEUSBkPC-zipG4KvM13ukDQWhe3A+v0Yvuh-3M07Z3PgfOPUYliY1eblys19B1kcttvlDtdnsB+MDlmECXkUf+cce6f6b2+0n+xffjErrMrhuDc2gLItyiiWZqjLWZ9yWMENhgSdEArfBG2PR9cDg5YBUQrZEEbM921wTtu17W9mSHSVn1kV9J09D9Zy-MNfzDAD8GwKBmHdP8eSDEEcNWABuTjuLuBtEzlfiUUTMkZN+AEoiBZp9EtAB+dxcA7MAJBAMlRJ47CEM2ZDPkk4NcETBTAWBflVlwDSrO03SgA).

```tsx
createColors({
  base: { primary: "red" },
  derived: {
    primary400: lightenWithNoInfer("primary", 0.4), // does not compile
    primary500: lightenWithoutNoInfer("primary", 0.5), // does not compile
    primary600: lightenWithoutNoInfer("primary" as const, 0.6),
    primary700: lightenWithoutNoInferWithInferStringLiteral("primary", 0.7)
  }
})

declare const createColors: <C extends string>(theme:
  { base: { [colorIdentifier in C]: Color }
  , derived: 
      { [derivedColor in string]: 
          { base: NoInfer<C> // `NoInfer` so that `C` is inferred only from `base`
          , operation: (c: Color) => Color
          }
      }
  }) => "TODO"

declare const lightenWithNoInfer:
  <Z>(base: NoInfer<Z>, weight: number) => 
    { base: Z
    , operation: (c: Color) => Color
    }

declare const lightenWithoutNoInfer:
  <Z>(base: Z, weight: number) => 
    { base: Z
    , operation: (c: Color) => Color
    }

declare const lightenWithoutNoInferWithInferStringLiteral:
  <Z>(base: InferStringLiteral<Z>, weight: number) => 
    { base: Z
    , operation: (c: Color) => Color
    }
  
type Color = string;
type NoInfer<T> = [T][T extends any ? 0 : never]
type InferStringLiteral<T> = T extends string ? T : never
```

None of the `lighten` version is good.

1. `lightenWithNoInfer` has completions but are incorrect and doesn't compile
2. `lightenWithoutNoInfer` infers `Z` as `string` instead of `"primary"` hence doesn't compile
3. `lightenWithoutNoInferWithInferStringLiteral` compiles but has no completions

If I were to be frank, there is no rocket science going on here, `lightenWithoutNoInfer` (or at least `lightenWithNoInfer`) should "just work" with completions and compilation.

And the problem isn't about string literals per se. Here's another real world example from xstate. [Playground](https://www.typescriptlang.org/play?jsx=0&ts=4.4.0-beta#code/MYJwpghgLmCyHABYEsB2YAUBvAUAAjwGckwBbCALjyzzADcxUorRIYBlE8gHhqgE8ADmCoAiAGIBRACoBhABKi8AXwB8GAJQqANPjxpkUZBAA2Y5ABMTYUboKEo0MISq4CBS9dd73tJiH4qQkYLbDwBYTEpOUUVDTtfPAB7VG9E92iFMQAzMCgkNABzUR93ZVLlBIJc-JRUQtdygnLlDRwcCzBgEwhwPGAUh37wJ3gC9CpudkcYWgAPGFQLQiIoECLtPEkGJnnF5epwoRFV9fqVdVIKPRpiRDJKQ-pGZi2dqBU8AHovvAB3MD-CC7AAG2xeIPCSTwACNAWhciBwBZkqgTPw8NkQElSHh7uA9JsDEZTFQAHJJACSqERUxmYFUhNWThcpRoAG12PpUHhpk4ALpUH7-QF-YEfEF8mCQqDQuHcxHI1HozHY3H4sClXw0F4BAD8VC1iQAPocIidRHMHE4AHTBJYldLpTbPJjkqk0sAgbjgpiMp2JJqJTYpA1G7V4dmSblvF7s0Tm0T8-kGvAU6m0qUM8NlI1B5p6Vp4AC8qjwdCSlnanW6vUBA1QQ3tFmuBB96ldr3Tnu9klUWlLZuOYit9LtIVstHeVGj5Q6XR6fQbQ1YTk493Ik2k6gHZekOHNaY9tO3Jcj0n57OkexCK2BGL1eAADHgqOgGCB+QBuIA).

```tsx
createMachine({
  schema: { event: createSchema<{ type: "FETCH" }>() },
  initial: "idle",
  states: {
    idle: {
      entry: send({ type: "FETCH" }), // does not compile
      on: {
        FETCH: "fetching"
      }
    },
    fetching: {}
  }
})

declare const createMachine: <State extends string, Event extends { type: string }>(m:
  { schema: { event: Event } // we want `Event` to be inferred only from here
  , initial: NoInfer<State>
  , states:
    { [S in State]: // we want `State` to be inferred only from here
        { entry?: 
          | { type: "xstate.send"
            , event: NoInfer<Event>
            }
      , on?:
          { [E in Event["type"]]?: NoInfer<State>
          }
      }
    }
  }) => void

declare const send:
  <E>(event: NoInfer<E>) => { type: "xstate.send", event: E }

declare const createSchema: <T>() => T
type NoInfer<T> = [T][T extends any ? 0 : never];
```

The problem exactly is same as above. And here you can even inline `send({ type: "FETCH" })` to `{ type: "xstate.send", type: "FETCH" }` and it compiles and even provides completions. 

Aside: If we add `{ type: string }` to the `entry` union `send("")` [compiles](https://www.typescriptlang.org/play?jsx=0&ts=4.4.0-beta#code/MYJwpghgLmCyHABYEsB2YAUBvAUAAjwGckwBbCALjyzzADcxUorRIYBlE8gHhqgE8ADmCoAiAGIBRACoBhABKi8AXwB8GAJQqANPjxpkUZBAA2Y5ABMTYUboKEo0MISq4CBS9dd73tJiH4qQkYLbDwBYTElZQ07XzwAe1RvePcpOXkxADMwKCQ0AHNRH3dlEuU4ghy8lFQC1zKCMpicHAswYBMIcDxgJIde8Cd4fPQqbnZHGFoADxhUC0IiKBBC7TxJBiZZ+cXqcKERZdW6lXVSCj0aYkQySn36RmYNragVPAB6D7wAdzBfiDbAAGmyeQPCCTwACN-mgciBwBZEqgTPw8FkQAlSHhbuA9OsDEZTFQAHIJACSqHhEymYFU+OWThcJRoAG12PpUHhJk4ALpUL6-f4-QFvIE8mDgqCQmGc+GI5Go9GY7G4sAlXw0J4BAD8VA18QAPvsIkcHCcCioDb5jXxDmIZg4nAA6YILYqpVLrR5MUkUqlgEDcUFMeme+KNeLrJJ663uNmSTkvJ6s0Sm0S83l6vBkynUiV0uNNA2R4tNLQAXlUeDoCUsrXanW6-z6qAGboslwIwfUPueuYDQckqkr1btkTwokdtNdIVstFeVETZTaHS6PVbA1YTk4t3I42k6lHeGkOFNOf91MPeAreFZ0l5952ISWgLROrwAAY8FR0AwQLyADcQA) but `{ type: "xstate.send", event: "" }` [doesn't](https://www.typescriptlang.org/play?jsx=0&ts=4.4.0-beta#code/MYJwpghgLmCyHABYEsB2YAUBvAUAAjwGckwBbCALjyzzADcxUorRIYBlE8gHhqgE8ADmCoAiAGIBRACoBhABKi8AXwB8GAJQqANPjxpkUZBAA2Y5ABMTYUboKEo0MISq4CBS9dd73tJiH5XPAFhMQAPBycAOkJGC1taBiYgkJE8USVlHR93AHtUb19fKTl5MQAzMCgkNABzURyCZRzlO3dK6pRUWtdmppxlDRwcCzBgEwhwPGB8h2nwJ3ga9CpudkcYWjCYVAtCIigQOu08SSSoLZ296mChNIcj7pV1Ugo9GmJEMkob+kZmU7nFR4AD0ILwAHcwJCIEw8AADM7-eHBXJ4ABG0LQlRA4AseHyJn4eHKIFypDwX3AehOBiMpioADlcgBJVA4tYbMCqGkHJwuHI0ADa7H0qDw6ycAF0qGDIdCIbCLvDJTAUVA0ZixTi8QTUESSWSKVSwI13DR-gEAPxUM2+AA+N1SVAedRUdvcjr4d3CkRgMTiDSKwbwJz+yTwzLZHKRTB5IaKfSKJ3yNo9BGFkjFgP+QtEqVEUqlNsjrPZYBAnKc8YT7uDSf6TS0AF5VHg6LlLMNRuNJtCZqg5rFdm8CNxJOpwwCo+XKxOW23vaF0hEuQHdgkp1Qs80RmMJlMB3NWE5OF9yKtpOoF3hpDhUqXoxXuFe8M28ELpFLP5c4vtYcSVp4AADHgVDoAwIBSgA3EAA), which is kinda weird too because both are equivalent.

## 💻 Use Cases

Any case where the type parameters of a function are to be inferred from the return type instead of parameters AND location of the function call is a generic; is a use case. I suspect this improvement/bugfix will have a huge impact especially for library authors. Probably there are some folks out there banging theirs heads to make the completions work when they should probably "just work" without having to do anything.

### Some "workarounds"

For s5

```tsx
declare const $:
  < R
  , Z extends R extends { $: infer X } ? X : never>
    (x: Z) => R & { $: Z }
```

For `lighten`

```tsx
declare const lighten:
  < R
  , C extends R extends { base: infer X } ? X : never>
    (base: C, weight: number) => 
      & R
      & { base: C
        , operation: (c: Color) => Color
        }
```

For `send`

```tsx
declare const send:
  < R
  , E extends R extends { event: infer X } ? X : never>
    (event: E) =>
      & R
      & { type: "xstate.send", event: E }
```

All the above provide completions and compile too. Though complex cases like s4 don't work with this workaround.

Thanks for reading!