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

generic bound doesn't work when used in index type with concrete index type #51394

Closed
DetachHead opened this issue Nov 3, 2022 · 3 comments
Closed
Labels
Duplicate An existing issue was already created

Comments

@DetachHead
Copy link
Contributor

Bug Report

πŸ”Ž Search Terms

index type generic

πŸ•— Version & Regression Information

4.9.0-dev.20221025

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

interface Foo {
  a: number[]
  b: string[]
}

declare const foo: Foo

const setValue = <T extends keyof Foo>(key: T, value: Foo[T][number]) => {
  foo[key] = [value]
}

πŸ™ Actual behavior

  Type '(string | number)[]' is not assignable to type 'Foo[T]'.
   Type '(string | number)[]' is not assignable to type 'number[] & string[]'.
     Type '(string | number)[]' is not assignable to type 'number[]'.
       Type 'string | number' is not assignable to type 'number'.
         Type 'string' is not assignable to type 'number'.

πŸ™‚ Expected behavior

no error, since it works when the type of value is Foo[T]:

const setValue = <T extends keyof Foo>(key: T, value: Foo[T]) => {
  foo[key] = value
}
@jcalz
Copy link
Contributor

jcalz commented Nov 3, 2022

This is similar to #30581 and as such the current non-type-assertion solution is to refactor as mentioned in #47109, but the cure might be worse than the disease.


So, TS is protecting you from doing this (unlikely in practice) terrible thing:

const setValue = <K extends keyof Foo>(key: K, value: Foo[K][number]) => {
  foo[key] = [value] // compiler error
}
setValue(Math.random() < 0.999 ? "a" : "b", "oopsie"); // no compiler error
foo.a.map(x => x.toFixed()) // 99.9% chance of runtime error

Of course that same problem can happen in the version that "works":

const setValue = <K extends keyof Foo>(key: K, value: Foo[K]) => {
  foo[key] = value // no compiler error
}
setValue(Math.random() < 0.999 ? "a" : "b", ["oopsie"]); // no compiler error
foo.a.map(x => x.toFixed(2)) // 99.9% chance of runtime error

But, as described in #30769 (comment)

One rule we've always had is that any given type (and, by extension, any T[K] for identical T and K) is by definition assignable to itself, and that's the basic unsoundness we permit.
So assignment lhs = rhs is allowed if typeof lhs and typeof rhs are considered identical by the compiler, even if it would be unsound to allow it.

The version that works has identical Foo[K] on both sides, but the one that fails has Foo[K] on the left and Foo[K][number][] on the right, so they are not identical. You could rewrite your types as described in #47109 so that the compiler sees the assignment as being identical types on both sides, which involves multiple reassignments (but no type assertions):

type _Foo<K extends keyof Foo = keyof Foo> = { [P in K]: Foo[P][number][] };
const _foo: _Foo = foo; // no compiler error
const setValue = <K extends keyof Foo>(key: K, value: Foo[K][number]) => {
  const __foo: _Foo<K> = _foo; // no compiler error
  __foo[key] = [value]; // no compiler error
}

But, uh, all that to work around stuff so that it permits unsoundness without type assertions is a little much; you might as well just assert and move on:

const setValue = <K extends keyof Foo>(key: K, value: Foo[K][number]) => {
  foo[key] = [value] as Foo[K]; // no compiler error
}

It would be nice if there were something better here, but I don't know what that would be.

Playground link

@fatcerberus
Copy link

#30769 is related, as it's the source of the intersection behavior you see (number[] & string[]) in the OP. Prior to that PR being merged, patterns like this actually used to work.

So, TS is protecting you from doing this (unlikely in practice) terrible thing

I'm not convinced it's that unlikely either. While nobody is going to write Math.random() < 0.999 ? "a" : "b" for the key, they may be passing the key in via a variable typed as keyof Foo and then all bets are off (worst case scenario: one of the properties is typed as unknown and now you can pass in anything at all for the value). I feel like the "correct" (i.e. most elegant) solution to this problem would be to implement oneof constraints, wherein the compiler can know for sure that T[K] isn't going to be a union of several property types.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Nov 19, 2022
@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

5 participants