When creating a mapped type using the syntax {[K in keyof T]: ⋯}
, where in keyof
is included, TypeScript interprets it as a homomorphic mapped type (refer to What does "homomorphic mapped type" mean?) and applies specific operations based on the type in T
. Typically, this involves maintaining the optional/readonly modifiers from the properties of T
in the resulting mapped type.
In contrast, if the mapped type resembles {[K in KK]: ⋯}
where KK
is any keylike type not starting with keyof
, TypeScript cannot determine the object type T
associated with the keys in KK
, leading it to map over the keys in KK
independently of T
.
This behavior is well-documented, especially in cases where T
is a generic type parameter instantiated with an object type. When T
is a generic type parameter instantiated with an array/tuple type, or with a primitive type, the homomorphic mapping behaves differently compared to non-homomorphic mapping.
In your scenario, where the type T
is defined as any
, it represents a unique case. TypeScript treats {[K in keyof any]: ⋯}
as a homomorphic mapped type across any
, resulting in {[k: string]: ⋯}
following the implementation outlined in microsoft/TypeScript#19185, which corresponds to your T1
type.
This interpretation was implemented prior to numeric/symbol keys in keyof
and mapped types as realized in microsoft/TypeScript#23592, and considerably ahead of symbol or union index signatures as introduced in microsoft/TypeScript#44512. Consequently, the behavior of homomorphic and non-homomorphic mapped types has slightly diverged for keyof any
since then.
Perhaps one could submit a feature request to rectify this inconsistency, although it appears to be a rare scenario that few encounter or find problematic. Neither behavior is outright incorrect, and the discrepancy is just among the various inconsistencies in TypeScript, thus differing behaviors do not signify a bug (e.g., varying behavior is not necessarily a bug).
If you truly intended to obtain the version containing three index signatures and did not intend for the homomorphic mapping, you can sever the link by introducing an alias for keyof any
without using in keyof
. One way to accomplish this is by employing simple parentheses:
type T3 = { [P in (keyof any)]: number };
/* type T3 = {
[x: string]: number;
[x: number]: number;
[x: symbol]: number;
} */
Instead of keyof any
, utilizing the built-in PropertyKey
type provided by TypeScript is recommended, declared as such within the TypeScript library at
declare type PropertyKey = string | number | symbol;
This approach yields
type T4 = { [P in PropertyKey]: number };
/* type T4 = {
[x: string]: number;
[x: number]: number;
[x: symbol]: number;
} */
The only situation warranting the use of keyof any
over PropertyKey
is when code needs to function both before and after support for number
and symbol
keys in mapped types, covering TypeScript versions prior to and post 2.9. However, this circumstance is highly improbable at present. For recent TypeScript versions, opt for PropertyKey
instead of keyof any
.
Link to playground for code