Lesson 1 of 9
Mapped Types
"The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise." — Edsger W. Dijkstra
Part III taught you to ask questions of a type: keyof for its keys, T[K] for the value at a key, generics to carry a type through a function. Those were the foundation. Now we do something new — we transform a type into another type, programmatically, one key at a time. A mapped type is a for loop that runs in the compiler: for every key in this shape, produce a property in the new shape. It is how Partial, Readonly, Pick, and Record are actually built, how form libraries turn a model into a "patch" type, and how a DTO is derived from a database row without a single hand-typed field. This is the moment TypeScript stops describing types and starts manufacturing them.
You have one shape. You need a dozen close relatives of it — all-optional for a patch, all-readonly for a snapshot, all-nullable for a form. How do you derive every one of them from the original, so they update themselves when it changes?
Core Concepts
The syntax: a for-each over keys
A mapped type has one signature you will learn to read instantly: { [K in keyof T]: … }. Read it as "for each key K in the keys of T, create a property named K whose type is whatever the right-hand side computes." The in here is the type-level cousin of for…in — it iterates a union of keys, binding K to each one in turn.
Clone<T> does nothing but restate T — and that is the point: it shows the skeleton before we start changing things. MyPartial and MyReadonly add exactly one token each (? and readonly), and suddenly they are the standard library's Partial and Readonly, character for character. When you write Partial<Person>, this is the machinery that runs.
A mapped type is a factory production line for types. The original type is the template part; the mapping is the machine each key passes through. Send every key down the "make it optional" line and you get a form-patch type; send them down the "freeze it" line and you get a snapshot type. You don't hand-build each variant — you describe the machine once and feed the shape in. Change the template part upstream and every product downstream changes with it, no retooling required.
Modifiers: adding and removing readonly and ?
The two modifiers — readonly and ? — can be added, as above, but the powerful move is removing them with the - prefix. -readonly strips read-only-ness; -? strips optionality (making a field required). This is how Required<T> is built, and how you write a "make it writable again" helper:
Notice the modifiers are independent: Mutable strips readonly but leaves ? alone, while Concrete strips ? but leaves readonly alone. Plain readonly/? with no - add the modifier; the - prefix removes it. That symmetry is the whole modifier system.
Mapped types shipped in TypeScript 2.1 (December 2016), in the same release as keyof and indexed access — the three were designed as one feature. That release is why lib.es5.d.ts can define Partial, Readonly, Pick, and Record in TypeScript itself rather than baking them into the compiler. The modifier-removal syntax (-readonly, -?) came a version later in 2.8, alongside conditional types — and key remapping with as waited until 4.1 (2020). Each release taught the type system a little more of the vocabulary you're learning in this chapter.
Transforming the value types
Nothing forces the right-hand side to be T[K] unchanged — that is where mapped types earn their keep. Wrap, union, or replace each value type to derive a whole family of shapes:
That last block is worth pausing on: K in "read" | "write" | "admin" iterates a literal union directly, not keyof anything. Generalise it — { [K in Keys]: V } — and you have written Record<Keys, V> by hand. Record is nothing more than a mapped type over the union of keys you hand it.
Key remapping with as: rename and filter keys
So far the keys passed through untouched. The as clause — { [K in keyof T as NewKey]: … } — lets you rename each key, or drop it entirely by remapping it to never. This is the mapped type at full power:
Two techniques in one block. Filtering: remap a key to never and it vanishes from the output — that conditional K extends "password" ? never : K is a preview of conditional types, the very next chapter. Renaming: the as `get${Capitalize<string & K>}` produces getId, getName — a template-literal transform, the subject of chapter 4. Together they turn a database User into a password-free DTO and a getter-based API, each derived automatically from the one source shape.
How mapped types show up in real work:
• "Make everything optional for a patch." A PATCH endpoint takes Partial<Row> — every column optional — so a caller can send just the fields that changed, type-checked against the real table shape.
• Derive DTOs, never duplicate them. The public API type is a key-remapped view of the internal row (drop password, drop internalNotes) — one edit to the row updates both.
• Remember what modifiers do. No prefix adds; - removes. { [K in keyof T]-?: T[K] } is Required; { -readonly [K in keyof T]: T[K] } thaws a frozen type.
• Map a union directly for Record-shapes. Permission maps, lookup tables, and config-by-key are all { [K in Keys]: V }.
The Academy is a Next.js app on Drizzle and MySQL, and Drizzle infers a row type per table — say Student = { id: number; email: string; passwordHash: string; score: number }. The "edit profile" form doesn't hand-write its own type; it uses Partial<Student> so a student can submit only the fields they changed. The public profile returned from the API is a key-remapped view that drops passwordHash — remap that key to never and it can't leak, because it no longer exists in the type. Add a column to the table and both the patch type and the public DTO update themselves. No field is ever written twice, so none can ever fall out of sync.
Common Mistakes
- Forgetting the
-to remove a modifier. Plain?andreadonlyonly ever add. To buildRequiredor a mutable type you need-?and-readonly; writing?where you meant-?does the opposite of what you intended. - Expecting a plain conditional to filter keys. A
nevervalue type still leaves the key present (asnever). To actually drop a key you must remap it in theasclause —as K extends … ? never : K— not just set its value tonever. - Mapping over
Tinstead ofkeyof T.[K in T]is wrong whenTis an object type; you iterate keys with[K in keyof T]. You use[K in SomeUnion]only when the thing you're iterating is already a union of keys. - Losing precision by widening the value.
{ [K in keyof T]: any }throws away everything the map was for. Keep the value tied to the key withT[K](optionally wrapped), so each property keeps its exact type. Capitalizeon a non-string key. Keys can bestring | number | symbol; template-literal renaming needs a string, so intersect withstring(Capitalize<string & K>) or the compiler rejects the numeric/symbol case.- Assuming a mapped type keeps methods and unions tidy. Mapping a union type distributes per-member in ways that can surprise you; when a shape is a union, reason about one member at a time.
You have type Row = { readonly id: number; name?: string; email?: string }. Write, in your head, three derived types with mapped syntax: (1) a fully-writable, fully-required version for seeding test data; (2) an all-optional patch version; (3) a "form draft" where every field may also be null. Now the deeper question: if you later add a readonly createdAt: Date column to Row, how many of your three derived types need editing? (Zero — every one recomputes from keyof Row. That is the entire reason to derive instead of hand-write.)
Interview Corner
Partial with one?"{ [K in keyof T]: T[K] } — 'for each key K, make a property K.' Partial just adds the optional modifier: { [K in keyof T]?: T[K] }. Every field becomes optional."Readonly." → { readonly [K in keyof T]: T[K] } — add the readonly modifier instead.? and readonly add them; the - prefix removes them. So { [K in keyof T]-?: T[K] } strips optionality — that's Required — and { -readonly [K in keyof T]: T[K] } strips read-only. They're independent, so I can remove one and leave the other."as clause to never: [K in keyof T as K extends "secret" ? never : K].passwordHash to never, so they can't leak by construction. The patch type is Partial<Row>. The draft is { [K in keyof Row]: Row[K] | null }. Because all three are computed from keyof Row, adding a column updates every derived shape automatically — the compiler enforces the single-source-of-truth discipline instead of code review. That's how you stop DTO drift on a big team."Partial or Readonly from scratch in the editor and narrate the [K in keyof T] loop out loud as you type it.-? do versus ??" and "why does a never value not remove the key, but a never in as does?" Name the mechanism, don't wave at it.Pick, Record, Required) from mapped syntax and explain the release history if it comes up.- In the editor above, write
MyRequired<T>using-?and apply it toConfig; hover the result and confirm both fields became required whilereadonlystayed. - Add a
role: stringfield to thePersonin the getters block and confirmGetters<Person>grows arole: () => stringproperty on its own — you edited one type, not two. - Extend
PublicUserto also drop a newinternalNoteskey using theas … neverpattern, then try to addinternalNotestosafeand read the exact error. - Out loud, as in a micro1 screen, explain in one breath the difference between
{ [K in keyof T]?: T[K] }and{ [K in keyof T]-?: T[K] }.
Chapter Summary
- A mapped type —
{ [K in keyof T]: … }— is a type-levelforloop: for each key ofT, produce a property in a new type. - Add modifiers with
readonlyand?; remove them with-readonlyand-?. This is exactly howPartial,Readonly, andRequiredare built. - The value side is free to transform:
T[K] | null,() => T[K], or any wrapper derives a whole family of related shapes. - Iterate a literal union directly —
{ [K in Keys]: V }— and you've writtenRecordby hand. - Key remapping with
asrenames keys (via template literals) or drops them by mapping tonever— the engine behind DTOs and password-free public types. - Derive shapes instead of hand-writing them: one source of truth, and every relative recomputes when it changes — drift made structurally impossible.
You saw it twice already — K extends "password" ? never : K inside an as clause. That X extends Y ? A : B is a conditional type: an if/else that runs in the compiler and chooses one type or another based on how types relate. It's what let us filter keys, and it's the last great primitive of type-level programming — powering Exclude, Extract, ReturnType, and the inference tricks that read a type out of another. That is Part IV, chapter 2: Conditional Types.
Editorial reflection — mapped types mark a quiet promotion in what you are as a programmer. Up to now you wrote types; here you write programs that write types — a loop, a transform, a decision, all executed by the compiler before your code ever runs. The temptation is to see this as cleverness for its own sake, and misused it becomes exactly that. But its real purpose is humbler and more durable: to let a codebase hold one authoritative shape and derive the rest, so that the truth is stated once and every consequence follows automatically. The best type-level code, like the best code of any kind, is not the code that shows off how much it can compute — it's the code that means you never have to remember to keep two things in step again.
Q&A · 0
Enrol to ask questions and join the discussion.
No questions yet — be the first to ask.