In TypeScript, what does a mapped type written as `{ [K in keyof T]: ... }` over a type parameter `T` preserve that a mapped type written as `{ [K in 'a' | 'b']: ... }` cannot, and why does that matter when `T` is an array or a tuple?
answer
- where the key set comes from decides everything
- keys sourced from keyof T
- modifiers copied from the source type
- arrays stay arrays, tuples stay tuples
- a fixed key union builds a fresh object
basics
~20 sMapping over keyof T is homomorphic: it copies each property's readonly and optional modifiers from T, and it maps arrays to arrays and tuples to tuples. Mapping over a fixed literal key union builds a brand-new object type with everything required, mutable and object-shaped.
solid answer
~50 s`{ [K in keyof T]: ... }` where `T` is a type parameter is a **homomorphic** mapped type: the checker remembers that the keys came from `T`, so each resulting property keeps the `readonly` and `?` modifiers it had in the source, and the mapping is applied *through* array and tuple structure — `Clone<string[]>` is `string[]`, and `Clone<[number, string]>` stays a two-element tuple rather than becoming an object with numeric keys plus `length` and the array methods. It also distributes over a union argument. A mapped type over a fixed key union like `{ [K in 'a' | 'b']: ... }` has no source type to copy from, so every member comes out required and mutable and the result is always a plain object type. That is why derived-type helpers are almost always written against `keyof T` — preserving the source's shape for free is the whole point.
code
typescript · 18 linestype Boxed<T> = { [K in keyof T]: { value: T[K] } };
interface Source {
readonly id: number;
nickname?: string;
}
// modifiers survive without being mentioned
type BoxedSource = Boxed<Source>;
// { readonly id: { value: number }; nickname?: { value: string | undefined } }
// arrays stay arrays, tuples stay tuples
type BoxedArray = Boxed<string[]>; // { value: string }[]
type BoxedTuple = Boxed<[number, string]>; // [{ value: number }, { value: string }]
// a fixed key union has no source to copy from
type Flags = { [K in "read" | "write"]: boolean };
// { read: boolean; write: boolean } — required and mutablego deeper
Know that { [K in keyof T]: ... } walks a type's properties and produces one member per key, and that the whole construct disappears from the emitted JavaScript.
Explain that mapping over keyof T copies each property's readonly and optional modifier from the source, while mapping over a fixed key union has no source and produces required, mutable members.
Demonstrate the array and tuple case — Clone<string[]> staying string[] — and diagnose a helper that lost it, since that bug hides behind object-shaped test cases and only appears on real inputs.
Own the API guidance: transform helpers in a shared library should derive from keyof T so callers' modifiers and collection shapes survive, and any exception should be a documented decision rather than an accident of how someone wrote the key set.
## Two shapes of mapped type A mapped type iterates a key set and produces a property per key. The syntax is the same either way, but the *source* of the key set changes the semantics profoundly. ```ts // (1) keys taken from a type parameter type Clone<T> = { [K in keyof T]: T[K] }; // (2) keys given as a fixed union of literals type Flags = { [K in "read" | "write"]: boolean }; // => { read: boolean; write: boolean } ``` Form (1) is called **homomorphic**: it maps over the keys of some other type, and the compiler keeps a link back to that type. Form (2) constructs a fresh object type out of thin air. Everything below follows from that link existing or not. ## What the homomorphic form preserves — modifiers ```ts interface Source { readonly id: number; nickname?: string; } type Copy = Clone<Source>; // { readonly id: number; nickname?: string } ``` `Clone` never mentions `readonly` or `?`, yet both survive. Because the key set is `keyof Source`, the checker knows which property each mapped member came from and copies its modifiers across. This is the default and it is usually what you want: a helper that transforms property *types* should not silently make optional fields required or drop immutability. (Deliberately adding or removing those modifiers is a separate mechanism with its own syntax; the point here is that doing nothing preserves them.) ## What it preserves — array and tuple structure This is the part that catches people out. An array type technically has a numeric index signature plus `length`, `push`, `map` and the rest. Naively mapping over its keys would produce a hideous object type with every array method mangled. Homomorphic mapping does not do that — it maps the *element* type and hands back an array: ```ts type Boxed<T> = { [K in keyof T]: { value: T[K] } }; type A = Boxed<string[]>; // { value: string }[] type B = Boxed<[number, string]>; // [{ value: number }, { value: string }] ``` Arrays stay arrays, tuples keep their length and their positions. This special case is what makes generic transform helpers usable on collection types at all, and it applies only to the literal `{ [K in keyof T]: ... }` form over a type parameter. Build the key set some other way and you get an object type instead. ## What it preserves — union distribution A homomorphic mapped type applied to a union distributes over the members rather than mapping the union's common keys: ```ts type C = Boxed<{ a: string } | { b: number }>; // Boxed<{ a: string }> | Boxed<{ b: number }> ``` That matters because `keyof` on a union is only the *shared* keys; without distribution, mapping a union would quietly discard the members' own properties. ## The fixed-key-union form ```ts type Keys = "a" | "b"; type Rec = { [K in Keys]: number }; // { a: number; b: number } ``` Here there is no source type. The compiler has nowhere to look up a `readonly` or a `?`, so every property is required and mutable — not because it decided to strip them, but because there is nothing to inherit. The result is always a plain object type; there is no way for this form to produce an array or a tuple, even if the values happen to be array types. This form is the right tool when you are *building* a shape from a known key set rather than *deriving* one from an existing type. ## Choosing between them Ask what the helper is for. - **Deriving from an existing type** — a transform that should behave like "the same shape, different value types" — write `{ [K in keyof T]: F<T[K]> }`. You get modifier preservation, array and tuple handling, and union distribution for free, and callers are not surprised when their `readonly` fields stay `readonly`. - **Constructing a shape from a key set** — a registry, a handler map, a set of flags — write the key union explicitly. You want uniform required members and you are not describing a transformation of anything. The failure mode is reaching for the second when you meant the first. A helper that rebuilds the property list by hand loses the array and tuple handling of every input type it is applied to, and callers get an unusable object type back. The bug does not show up in a simple `{ a: string }` test case, which is exactly why it survives review — it appears the first time someone applies the helper to a tuple. All of this is compile-time only. Whichever form you choose, the emitted JavaScript is unchanged: mapped types describe shapes for the checker and vanish at emit.
- What happens when a homomorphic mapped type is applied to a union type?It distributes: the mapping is applied to each member and the results are unioned back together. That is important because `keyof` on a union yields only the shared keys, so a non-distributing mapping would drop every property the members do not have in common. Distribution keeps each member's own shape intact.
- If you build the key set some other way instead of writing `keyof T` directly, what concretely breaks?You lose array and tuple handling — passing an array produces a plain object type built from its numeric index and methods rather than an array, and a tuple loses its length and positions. Simple object inputs still look fine in tests, which is why the defect usually surfaces only when someone applies the helper to a collection type.
- When is the fixed-literal-union form the right choice?When you are constructing a shape rather than deriving one: a handler registry keyed by event name, a flag set, a lookup whose keys are known up front. There is no source type whose modifiers you want to honour, and uniform required members are exactly what you are after. Using `keyof T` there would be indirection with no benefit.
saying these in an interview costs you the question
- Thinks mapping always strips readonly and optional modifiers
- Expects mapping an array to produce an object of numeric keys
- Believes both mapped-type forms behave identically
- Says the preservation is a special case for built-in helpers only
- Assumes mapped types leave something behind at runtime