Advanced Types

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.

The question this chapter answers

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.

TypeScript — Try it Yourself
Loading editor…
Type check + Console

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.

Everyday analogy

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:

TypeScript — Try it Yourself
Loading editor…
Type check + Console

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.

History changed here

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:

TypeScript — Try it Yourself
Loading editor…
Type check + Console

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:

TypeScript — Try it Yourself
Loading editor…
Type check + Console

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.

The engineer's notebook

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 }.

Real-world example — chika.ng Academy

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 ? and readonly only ever add. To build Required or 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 never value type still leaves the key present (as never). To actually drop a key you must remap it in the as clause — as K extends … ? never : K — not just set its value to never.
  • Mapping over T instead of keyof T. [K in T] is wrong when T is 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 with T[K] (optionally wrapped), so each property keeps its exact type.
  • Capitalize on a non-string key. Keys can be string | number | symbol; template-literal renaming needs a string, so intersect with string (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.
Thought experiment

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

Interview Corner — the same idea at three levels. Answer yours; recognise the one above it.
Junior
"What is a mapped type, and can you write Partial with one?"
Model answer — "A mapped type builds a new object type by iterating the keys of an existing 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."
Follow-up they'll throw: "Now write Readonly." → { readonly [K in keyof T]: T[K] } — add the readonly modifier instead.
Intermediate
"How do you make a field required or writable in a mapped type — and how does that differ from making it optional?"
Model answer — "Modifiers can be added or removed. Plain ? 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."
Follow-up: "How would you drop a key entirely?" → Remap it in the as clause to never: [K in keyof T as K extends "secret" ? never : K].
Senior
"You maintain a database row type and need a public DTO, a patch type, and a form-draft type derived from it across a large codebase. How do you structure that?"
Model answer — "One source of truth — the row type, ideally inferred from the schema — and every other shape is a mapped type over it. The DTO is a key-remapped view that maps sensitive keys like 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."
The senior move: treat the schema as the one authored type and every transport/form/patch shape as a derivation, so security and consistency are structural, not remembered.
Company lens — how the same topic gets asked
micro1Live and conversational — expect to write Partial or Readonly from scratch in the editor and narrate the [K in keyof T] loop out loud as you type it.
GooglePrecision: "what exactly does -? 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.
MicrosoftMicrosoft authored TypeScript and shipped mapped types in 2.1 — expect them to ask you to reconstruct a utility type (Pick, Record, Required) from mapped syntax and explain the release history if it comes up.
Amazon / AWSDesign lens: "how do you keep a service's DTOs in sync with its models as it grows?" Tie mapped-type derivation to fewer runtime leaks and less duplication at scale.
Hands-on exercise
  1. In the editor above, write MyRequired<T> using -? and apply it to Config; hover the result and confirm both fields became required while readonly stayed.
  2. Add a role: string field to the Person in the getters block and confirm Getters<Person> grows a role: () => string property on its own — you edited one type, not two.
  3. Extend PublicUser to also drop a new internalNotes key using the as … never pattern, then try to add internalNotes to safe and read the exact error.
  4. 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-level for loop: for each key of T, produce a property in a new type.
  • Add modifiers with readonly and ?; remove them with -readonly and -?. This is exactly how Partial, Readonly, and Required are 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 written Record by hand.
  • Key remapping with as renames keys (via template literals) or drops them by mapping to never — 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.
Narrative bridge

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.