Skip to content

Hover display for non-homomorphic mapped types renders numeric-string keys indistinguishably from numeric keysΒ #63425

Open

Description

πŸ”Ž Search Terms

mapped type hover, numeric string key display, key quoting, homomorphic mapped type

πŸ•— Version & Regression Information

This is the behavior in every version I tried.

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/C4TwDgpgBAglC8UDeBtA0lAlgOygRgF0AuKNAXwG4AoAehqgYD0B+K0SKAIQWXS1wDkeAcVKVa9JsyA

πŸ’» Code

type A = {[K in 1]: K};
//   ^? type A = { 1: 1; }
type B = {[K in '1']: K};
//   ^? type B = { 1: "1"; }

πŸ™ Actual behavior

A and B have keys of different types (1 vs "1"), but the hover renders both keys identically as 1. In B, the display shows key 1 mapping to value "1" β€” the key and value originate from the same K, so they must be the same type, yet the display suggests they're not.

πŸ™‚ Expected behavior

The printer generally prefers the simpler key representation when it's unambiguous (abc rather than "abc"), but numeric literals are themselves valid keys, so numeric-looking strings need to stay quoted to avoid collapsing into a different type. This safeguard is applied correctly in the direct case:

type Direct = {'1': 'a'};
//   ^? type Direct = { '1': "a"; }

but is lost on the non-homomorphic mapped-type path. B's expected display is { '1': "1"; }.

Additional information about the issue

A related issue (#47214) discussed this for a simpler repro and was closed with the rationale that mapped-type results go through a normalization path without an original spelling to echo. That rationale fits cases where quoting is purely cosmetic; here the unquoted form renders B in a way that obscures which type its keys actually are, so the quoting isn't a style preference but part of what distinguishes string-keyed from number-keyed types in the display.

Note: {[K in keyof T]: ...} renders correctly because it matches the homomorphic mapped type pattern, which preserves the original key representation. Constraints that don't match this pattern, force the general normalization path where the quoting is lost.

Activity

  1. mkantor commented on Apr 23, 2026

    @mkantor
    Contributor

    You may already be aware of this, but since you haven't mentioned it: there is no such thing as a number key in JavaScript. All property keys are either strings or symbols at runtime; an object initializer like {[1]: 'a'} coerces the 1 to "1" (this is why Object.keys returns string[], for example). And the type system recognizes this, allowing {'1': 'a'}, {1: 'a'}, {[K in '1']: 'a'}, and {[K in 1]: 'a'} to all be mutually assigned to one another.

    However there are still observable differences between these types, e.g. keyof will give you either 1 or "1". I wish this wasn't the case (though I also wish fake number keys weren't a thing to begin with), but since it is I can understand the argument that the type display shouldn't obscure it.

    EDIT: I'm only now clicking through to #47214 where this is all explained, but I guess it bears repeating here.

  2. mkantor commented on Apr 23, 2026

    @mkantor
    Contributor

    Another way to phrase this issue is that B is displayed as a valid but observably-distinct type from what it actually is. Someone may innocently copy this type out of the type info but then get confused when it behaves differently from the original type (for example).

  3. codpro2005 commented on Apr 23, 2026

    @codpro2005
    Author

    You make excellent points, and in hindsight I should've included the type-system/runtime mismatch context in this issue itself. I opted to scope the issue narrowly to the hover inconsistency, since that's a non-breaking fix. I agree; the deeper fix would be for TypeScript to stop treating numeric literals as distinct key types at all, matching how JS actually behaves but I'm afraid that ship sailed a long time ago πŸ˜…

  4. RyanCavanaugh commented on Apr 23, 2026

    @RyanCavanaugh
    Member

    πŸ€– Thank you for your issue! I've done some analysis to help get you started. This response is automatically generated; feel free to πŸ‘ or πŸ‘Ž this comment according to its usefulness.

    Similar Issues

    Here are the most similar issues I found

    If your issue is a duplicate of one of these, feel free to close this issue. Otherwise, no action is needed.

  5. jcalz commented on Apr 24, 2026

    @jcalz
    Contributor

    Can we consider this a feature request instead of a bug? There's no guarantee that you can always copy a displayed type and have it be identical to the original type, but it seems preferable to do so where possible, all else being equal.

    Given that TypeScript does indeed distinguish between number literal keys and numeric string literal keys, it doesn't help me much to think of {0: any} and {"0": any} as just two different "spellings" of the same type. I mean, keyof {0: any} and keyof {"0": any} are not merely different spellings of the same type, they are different types.

    Personally I'd want to see a displayed key name in an object type correspond to at least one valid way of getting the same key in an object literal value. I'm going to say a spelling is "valid" if that works, and "invalid" if it does not. So I'm saying I want to see some valid spelling used for every key name in an object type. If there's an original valid spelling, then it can be used. Otherwise, some valid spelling should be chosen, presumably the simplest or most idiomatic one.

    Examples:

    • The simplest valid spelling of the type 0 is 0. It's valid because in const v = {0: ""}; type K = keyof typeof v, K is 0. Less idiomatic spellings are 0x0 and [0] and 0.000. But "0" is not a valid spelling because in const v = {"0": ""}; type K = keyof typeof v, K is not 0.
    • Simplest spelling of type -1 is [-1]. But "-1" is invalid.
    • Simplest spelling of type "2" is "2". But 2 is invalid.
    • Simplest spelling of type "-3" is "-3". But [-3] is invalid.
    • Simplest spelling of type "hello" is hello.
    • Simplest spelling of type "good-bye" is "good-bye".

    That implies I want to see something like

    type U = { [K in 0 | -1 | "2" | "-3" | "hello" | "good-bye"]: "" };
    /* type U = {
        0: "";
        [-1]: ""; 
        "2": ""; // not 2
        "-3": ""; // not [-3]
        hello: "";
        "good-bye": "";
    } */
    
    type V = { [-1]: "" };
    /* type V = {
        "[-1]": ""; // not "-1"
    } */
    
    type W = { [K in keyof ["", ""] as K extends `${number}` ? K : never]: "" }
    /* type W = {
        "0": ""; // not 0
        "1": ""; // not 1
    } */

    Does that seem like a reasonable request? Would it break anything somehow? Would it make some developer experience worse somewhere?

  6. RyanCavanaugh commented on Apr 24, 2026

    @RyanCavanaugh
    Member

    Joe Calzaretta (@jcalz) I remember the last time we poked at this, an enormous cloud of bees emerged, but I don't recall exactly what flavor. Worth poking again though

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

    Experimentation NeededSomeone needs to try this out to see what happensSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions