้•œๅƒ็ซ™็‚น ยท ๆœฌ้กต็”ฑ็ฌฌไธ‰ๆ–น GitHub ๅช่ฏป้•œๅƒๆไพ›๏ผŒ้ž GitHub ๅฎ˜ๆ–น็ซ™็‚น๏ผŒไธๆŽฅๅ—ไปปไฝ•็™ปๅฝ•ๆˆ–ๅ‡ญๆฎ่พ“ๅ…ฅใ€‚ๅ‰ๅพ€ github.com
Skip to content

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

Description

@DetachHead

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
}

Activity

  1. jcalz commented on Nov 3, 2022

    @jcalz
    Contributor

    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

  2. fatcerberus commented on Nov 3, 2022

    @fatcerberus

    #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.

  3. typescript-bot commented on Nov 21, 2022

    @typescript-bot
    Contributor

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

  4. locked as resolved and limited conversation to collaborators on Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions