Here’s a simple mapped type that uses keyof:
type Identity<T> = {
[P in keyof T]: T[P];
};
Why does Identity<number> return the primitive number type, rather than an object type? Is this special case for primitive types documented somewhere?
>Solution :
This is the intended behavior of homomorphic mapped types, as implemented in microsoft/TypeScript#12447 (this feature was originally called "isomorphic", but has since changed. Also see What does "homomorphic mapped type" mean?. And note that this term isn’t really present in the new version of the TS Handbook, although it is mentioned in the mapped types section in an older version).
According to that pull request:
when a primitive type is substituted for
Tin [a homomorphic] mapped type, we simply produce that primitive type. For example, when{ [P in keyof T]: X }is instantiated withA | undefinedforT, we produce{ [P in keyof A]: X } | undefined.
This behavior seems to primarily have been meant to deal with null and undefined in a reasonable way, as well as a plausible base case for recursive mapped types (and before condititional types were introduced, it was the only base case). See this comment on microsoft/TypeScript#13351:
It generally isn’t meaningful to apply mapped types to primitives, but in recursive types such as your example it is going to happen since types are rarely "objects all the way down".
and also this comment on microsoft/TypeScript#21840:
We have discussed this issue before, and the conclusion here is that mapped types [have] affordances to handling primitive types by design; we believe the vast majority of uses of mapped types are on object types, or are OK with skipping primitives (e.g. [
Readonly]).
The logic is: if TypeScript decides a mapped type is homomorphic, it will behave like an identity function for primitives. The exact situations where mapped types are seen to be homomorphic are hard to describe, but they’re essentially what you’re probing in your examples.