Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
TheMrZZ opened this issue Nov 23, 2020 · 5 comments
Labels
Domain: Completion Lists The issue relates to showing completion lists in an editor Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". Experience Enhancement Noncontroversial enhancements Experimentation Needed Someone needs to try this out to see what happens Help Wanted You can do this Suggestion An idea for TypeScript
Milestone

Comments

@TheMrZZ
Copy link

TheMrZZ commented Nov 23, 2020

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:

  • allergens is optionnal
  • We switch from an interface to a type
  • We use keyof this['quantities'] instead of INGREDIENTS[]
@andrewbranch
Copy link
Member

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.

@andrewbranch andrewbranch added Domain: Completion Lists The issue relates to showing completion lists in an editor Experience Enhancement Noncontroversial enhancements Experimentation Needed Someone needs to try this out to see what happens Suggestion An idea for TypeScript labels Nov 25, 2020
@andrewbranch andrewbranch added this to the Backlog milestone Nov 25, 2020
@andrewbranch andrewbranch added Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". Help Wanted You can do this labels Nov 25, 2020
@TheMrZZ
Copy link
Author

TheMrZZ commented Nov 29, 2020

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
Copy link

devanshj commented Jun 9, 2021

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
Copy link
Author

TheMrZZ commented Jun 9, 2021

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.

normal-array.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.

no-infer-arrays.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:

objects.mp4

@devanshj
Copy link

devanshj commented Jun 9, 2021

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Domain: Completion Lists The issue relates to showing completion lists in an editor Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". Experience Enhancement Noncontroversial enhancements Experimentation Needed Someone needs to try this out to see what happens Help Wanted You can do this Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants