typescriptkeyof

Differences between { [P in keyof any]: number } and { [P in string | number | symbol]: number }


I'm a beginner of typescript. I find a difference between keyof any and string | number | symbol in MappedType, but I'm not sure what the difference is between these two writing styles.

type T = keyof any; 
//string | number | symbol
type T1 = { [P in keyof any]: number };   
//{[x: string]: number}
type T2 = { [P in string | number | symbol]: number }
//{ [x: string]: number; [x: number]: number; [x: symbol]: number; }

Thank you all for your help!

I hope there are experts who can help me answer.


Solution

  • When you write a mapped type of the form {[K in keyof T]: ⋯}, with in keyof present, TypeScript treats it as a homomorphic mapped type (see What does "homomorphic mapped type" mean?) and performs some operations specific to the type in T. For normal cases this involves preserving the optional/readonly modifiers from the properties of T in the resulting mapped type.

    On the other hand if the mapped type just looks like {[K in KK]: ⋯} where KK is some arbitrary keylike type that doesn't start with keyof, then TypeScript doesn't have any idea what object type T the KK keys might have come from, and so it just maps over the keys in KK without any dependence on T.

    This is all documented behavior, especially for normal cases where T is a generic type parameter that is instantiated with an object type. When T is a generic type parameter that is instantiated with an array/tuple type, or with a primitive type, the homomorphic mapping will do different special things compared to the non-homomoprhic mapping.


    In your example, though, the type T is any, and this seems to be a corner case. TypeScript is treating {[K in keyof any]: ⋯} as a homomorphic mapped type over any, and it produces {[k: string]: ⋯} as implemented in microsoft/TypeScript#19185, which is responsible for your T1 type.

    This was implemented before numeric/symbol keys in keyof and mapped types as implemented in microsoft/TypeScript#23592, and very much before symbol or union index signatures as implemented in microsoft/TypeScript#44512. At the time there was no concept of {[k: string]: ⋯; [k: number]: ⋯; [k: symbol]: ⋯} as you see in your T2 type. So over time the behavior of the homomorphic and non-homomorphic mapped type have diverged a bit for keyof any.


    It's possible one could file a feature request asking for this discrepancy to be addressed, although it really seems like a corner case that few people run into or have trouble with. Neither behavior is obviously wrong, and the inconsistency is just one of many inconsistencies in TypeScript, so the fact that they differ is not enough to make it a bug (e.g., "different code does different things" is not necessarily a bug).

    Anyway, assuming you really wanted to get the version with the three index signatures and that the homomorphic mapping was unintentional, you can break the connection by using some alias for keyof any so that in keyof doesn't appear. You can apparently do this with simple parentheses:

    type T3 = { [P in (keyof any)]: number };
    /* type T3 = {
        [x: string]: number;
        [x: number]: number;
        [x: symbol]: number;
    } */
    

    But instead of keyof any I'd recommend using the TypeScript-provided PropertyKey type, which is declared in the TypeScript library as

    declare type PropertyKey = string | number | symbol;
    

    So you get

    type T4 = { [P in PropertyKey]: number };
    /* type T4 = {
        [x: string]: number;
        [x: number]: number;
        [x: symbol]: number;
    } */
    

    The only time you'd really want to write keyof any instead of PropertyKey is if you have code that needs to work both before and after number and symbol keys were supported in mapped types, meaning code both before and after TypeScript 2.9. Which is quite unlikely at this point. If you're only looking at recent TypeScript versions, then just write PropertyKey instead of keyof any.

    Playground link to code