microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.54k stars 12.43k forks source link

Incorrect inference/autocompletion on generic arrays, when values can be inferred from a defined object. #41645

Open TheMrZZ opened 3 years ago

TheMrZZ commented 3 years ago

TypeScript Version: 4.1.2

Search Terms: Autocompletion, incorrect, values, inference, generic, array, object, keys

Summary: When an interface/a type has an object with generic keys, and an array of those keys, the array values cannot be infered from the object keys.

Code

interface Recipe<INGREDIENTS extends string> {
  quantities: Record<INGREDIENTS, number>
  allergens?: INGREDIENTS[]
}

function createRecipe<INGREDIENTS extends string>(recipe: Recipe<INGREDIENTS>) {}

createRecipe({
  quantities: {
    eggs: 1,
    flour: 2,
  },
  allergens: ['']
})

Expected behavior: Here, when trying to give a value to allergens, the autocompletion should show "eggs" | "flour".

Actual behavior: The autocompletion doesn't find anything. image

Notes: The other way is working: you can fill the array first, then the object keys will autocomplete - but this rarely make sense to write things that way.

Failed workarounds: This bug is still present, even when:

andrewbranch commented 3 years ago

This is because the array is a stronger inference candidate than the record, so as soon as the array is non-empty, the type parameter gets instantiated to the types that can be inferred from its contents. The completions behavior you expect seems so obvious, but it’s actually really tricky to make it work. This is very similar to #36556 though, where we got reasonably good results, so maaaaybe that approach can be applied here.

TheMrZZ commented 3 years ago

Thanks for you complete and helpful answer! I understand that it's complicated to implement this feature properly, and event though I'd like to have that, I get that it will maybe never get implemented. It looks like a complicated problem, so no worries!

devanshj commented 3 years ago

Stumbled upon this... In case someone wants a workaround...

interface Recipe<Self> {
  quantities: Record<string, number>
  allergens?: Self extends { quantities: infer Q } ? (keyof Q)[] : never
}

function createRecipe<R extends Recipe<R>>(recipe: R) {}

createRecipe({
  quantities: {
    eggs: 1,
    flour: 2,
  },
  allergens: [""]
})

Hell wait much simpler workaround...

interface Recipe<I extends string> {
  quantities: Record<I, number>
  allergens?: NoInfer<I>[]
}
type NoInfer<T> = [T][T extends any ? 0 : never]

function createRecipe<I extends string>(recipe: Recipe<I>) {}

createRecipe({
  quantities: {
    eggs: 1,
    flour: 2,
  },
  allergens: [""]
})
TheMrZZ commented 3 years ago

This workaround reverses the problem. Using a normal array, the array has the priority. It means that, if you add a property to the object, the object will error out, and you won't have autocompletion on the array. However, adding a string to the array will error the object out, and you will have autocompletion on the object properties.

https://user-images.githubusercontent.com/30944236/121360699-f15fc580-c934-11eb-93c5-4e7781e6ccce.mp4

Using your workaround, the object gets the priority. It means that, if you add a property to the object, the array will error out, and you will have autocompletion on the array strings. However, adding a string to the array will error the array out, and you won't have autocompletion on the object properties.

https://user-images.githubusercontent.com/30944236/121360964-279d4500-c935-11eb-8d8e-068f255cee26.mp4

What should happen would be that changing one will error out the other, and autocompletion would always be provided. That currently works for objects:

https://user-images.githubusercontent.com/30944236/121361135-4ac7f480-c935-11eb-8df5-86eaa5d19a5f.mp4

devanshj commented 3 years ago

Ah my bad. Here's another workaround. You do get an error that property sugar is missing (so the types are correct) but there's no autocomplete, I suspect that's because of #44428 bug.