Skip to content

String index into a generic type having a string index signature isn't assignable to indexed access type #51127

Description

@jcalz

Bug Report

🔎 Search Terms

string index signature; indexed access; generic; 2322; not assignable

🕗 Version & Regression Information

This seems to be there all the way back to TS3.5, maybe introduced by #30769, although I'm not sure I understand why.

-This changed between versions 3.3.3 and 3.5.1

⏯ Playground Link

Playground link with relevant code

💻 Code

interface Foo {
    a: { [k: string]: string | undefined };
    b: { [k: string]: number | undefined };
    c: { [k: string]: boolean | undefined };
}
function get<K extends keyof Foo>(foo: Foo, k: K, x: string): Foo[K][string] {
    return foo[k][x]; // error!
    // Type 'string | number | boolean | undefined' is not assignable to type 'Foo[K][string]'.
}

🙁 Actual behavior

Reading a property with a string key from a Foo[K] object does not produce a value of type Foo[K][string], even in a non-widening position.

🙂 Expected behavior

Indexing into a Foo[K] value with a string key should produce a property of type Foo[K][string] if such a type is in a non-widening position.


This might be similar to #31661 and #33181 but I can't tell.

Note that numeric index signatures work as expected:

interface Foo {
    a: { [k: number]: string | undefined };
    b: { [k: number]: number | undefined };
    c: { [k: number]: boolean | undefined };
}
function get<K extends keyof Foo>(foo: Foo, k: K, x: number): Foo[K][number] {
    return foo[k][x]; // okay    
}

And known literal keys work as expected:

interface Foo {
    a: { x: string | undefined };
    b: { x: number | undefined };
    c: { x: boolean | undefined };
}
function get<K extends keyof Foo>(foo: Foo, k: K, x: "x"): Foo[K]["x"] {
    return foo[k][x]; // okay    
}

But string index signatures (and symbol also) fail for some reason.

This can be worked around easily enough:

function get<K extends keyof Foo>(foo: Foo, k: K, x: keyof Foo[K]): Foo[K][keyof Foo[K]] {
    return foo[k][x]; // okay
}

But I'm wondering what's going on.

Playground link with above examples

(note to self: go back to this SO answer if I acquire more cluefulness here)

Activity

  1. fatcerberus commented on Oct 10, 2022

    @fatcerberus

    It seems like maybe over-eager reduction to constraint, i.e. you're getting a concrete Foo[keyof Foo][string] instead of the generic Foo[K][string]

  2. RyanCavanaugh commented on Oct 10, 2022

    @RyanCavanaugh
    Member

    Indexing into a Foo[K] value with a string key should produce a property of type Foo[K][string] if such a type is expected from context.

    There's currently not any effect from contextual typing to whether indexed access produces a deferred or non-deferred type.

    It's pretty weird that there's a difference between string and number index signatures here, though. Anders Hejlsberg (@ahejlsberg) any thoughts on the motivation here?

  3. jcalz commented on Oct 10, 2022

    @jcalz
    ContributorAuthor

    There's currently not any effect from contextual typing to whether indexed access produces a deferred or non-deferred type.

    Really? Maybe I'm not using the term "contextual typing" properly? What accounts for this difference:

    interface Foo {
        a: { [x: number]: string | undefined };
        b: { [x: number]: number | undefined };
        c: { [x: number]: boolean | undefined };
    }
    function get1<K extends keyof Foo>(foo: Foo, k: K, x: number): Foo[K][number] {
        const fk: Foo[K] = foo[k];
        return fk[x];
    }
    function get2<K extends keyof Foo>(foo: Foo, k: K, x: number) {
        const fk: Foo[K] = foo[k];
        return fk[x]; //  string | number | boolean | undefined
    }

    Playground link to code

  4. RyanCavanaugh commented on Oct 10, 2022

    @RyanCavanaugh
    Member

    That's a widening, the same as you'd see in m here:

        function get2<K extends keyof Foo>(foo: Foo, k: K, x: number) {
            const fk: Foo[K] = foo[k]
            let m = fk[x];
        }
  5. jcalz commented on Oct 11, 2022

    @jcalz
    ContributorAuthor

    Okay, then I guess I meant "in a non-widening position" instead of "contextually" or something. Unless that's wrong too.

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

    Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.SuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions