This actually looks like a bug, see microsoft/TypeScript#27995.
Generally mapped types of the form {[K in keyof T]: ...T[K]...}
are considered to be homomorphic mapped types, and the structure of the input type T
is preserved as much as possible. This happens with optional and readonly
keys (see microsoft/TypeScript#12563), and was also the intent when mapped tuples/arrays were implemented in microsoft/TypeScript#26063.
In order for that to work, it means the compiler must look at [K in keyof T]
and remember to keep T
around after it has evaluated keyof T
. This happens when T
is a generic type, and for optional/readonly
keys this also happens when T
is some concrete type:
type MyObj = { a?: string, readonly b: number };
type MyMappedObj = { [K in keyof MyObj]: { value: MyObj[K] } }
/* type MyMappedObj = {
a?: {
value: string | undefined;
} | undefined;
readonly b: {
value: number;
};
} */
Note that this only works when your mapped type is explicitly iterating over keys with exactly "in keyof
". If you calculate your keys some other way, or assign them to a type alias, or even just parenthesize the keyof
expression, the spell is broken and the mapped type is no longer homomorphic:
type MyBadMappedObj = { [K in (keyof MyObj)]: { value: MyObj[K] } }
/* type MyMappedObj = {
a: { // not optional
value: string | undefined;
}
b: { // not readonly
value: number;
};
} */
So, mapping over keys like {[K in keyof T]: ...}
should preserve the structure of T
in the output.
Unfortunately, when the mapped arrays/tuples feature was introduced, it looks like this was only implemented for when T
is a generic type parameter, and not for a specific concrete type. In this comment, the implementer says:
The issue here is that we only map to tuple and array types when we instantiate a generic homomorphic mapped type for a tuple or array (see #26063). We should probably also do it for homomorphic mapped types with a keyof T
where T
is non-generic type.
and that's where it is for now. Maybe this will eventually be fixed. Until then, you should probably use an intermediate generic type like your Working
example as a workaround.
Playground link to code