Lesson 18 · Module 3.1 — Type-level programming
{ [K in keyof T]: … } walks the keysYou already loop over an object's keys at runtime with for (const k in obj).
A mapped type is the same loop, one level up — it walks every key of a type and rebuilds the
object, key by key. Pair it with indexed access T[K] (the type-level version of
obj[k]) and you can read most of the standard utility types on sight.
[K in keyof T] is a for-each over the keys of T.
Inside the body, K is the current key and T[K] is the type of the value at
that key. Whatever you write after the : becomes the new value type for that key.
keyof and T[K]Two operators do all the work. keyof T is the union of T's keys;
T[K] is the value type stored under key K. They are the type-level mirrors of
Object.keys and obj[k]:
type User = { id: number; name: string };
type Keys = keyof User; // "id" | "name" (a union of the key names)
type Val = User["name"]; // string (indexed access: the value's type)
type Any = User[keyof User]; // number | string (index by the whole union)
That last line matters for the rest of the module: indexing an object type by a union of keys gives you the union of the value types. Hold that thought.
A mapped type sits inside { … } and reads almost like English: for each K
in keyof T, the new value is …. Here is a Partial-like helper that makes
every property optional — the ? is the only change to the body:
type MyPartial<T> = {
[K in keyof T]?: T[K]; // for each key K of T → same value type, now optional
};
type P = MyPartial<User>;
// resolves to: { id?: number; name?: string }
Walk it by hand. keyof User is "id" | "name". The loop visits
K = "id" (value User["id"] = number), then K = "name"
(value string), copying each across with a ? bolted on. That is the entire
machinery behind the built-in Partial<T>.
The body after the : is an ordinary type expression, so you can transform each value.
Wrap every property in a getter function, reading T[K] for the original type:
type Getters<T> = {
[K in keyof T]: () => T[K]; // each value becomes a 0-arg function returning T[K]
};
type G = Getters<User>;
// resolves to: { id: () => number; name: () => string }
The shape (the keys) is preserved; only the value types change. When you read any mapped type, do
exactly this: name the key set on the left of in, then substitute one key at a time and see
what the body produces. Everything else in this module is a variation on these two moves.
[K in keyof T] (inside an object type) is the mapped-type loop — it declares a
key variable. T[K] is indexed access — it looks up a value type. Same square
brackets, different jobs: the first walks keys, the second reads a value.
Recall, don’t re-read. (Answers reveal on click.)
{ [K in keyof T]: T[K] }, what does the inner T[K] denote?MyPartial<{ a: number }> = { [K in keyof T]?: T[K] } resolve to?type U = { a: number; b: string }, what is U[keyof U]?The official handbook page is
TypeScript Handbook — Mapped Types,
with indexed access covered in
Indexed Access Types.
Best way to feel it: paste MyPartial and Getters into the
TS Playground and hover the result to watch each key
get rebuilt.
Staring at a utility type and unsure what the loop produces? Paste it into the Playground and walk
the keys one at a time. Next lesson (3.2) adds the as clause, which lets the loop
rename or drop keys instead of merely copying them.