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

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

Open
jcalz opened this issue Oct 10, 2022 · 5 comments
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@jcalz
Copy link
Contributor

jcalz commented Oct 10, 2022

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)

@fatcerberus
Copy link

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]

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Oct 10, 2022
@RyanCavanaugh
Copy link
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. @ahejlsberg any thoughts on the motivation here?

@jcalz
Copy link
Contributor Author

jcalz commented Oct 10, 2022

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

@RyanCavanaugh
Copy link
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];
    }

@jcalz
Copy link
Contributor Author

jcalz commented Oct 11, 2022

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
Labels
Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants