Skip to content

Utility, mapped and conditional types

A mapped type loops over keys; a conditional type tests, per union member; infer reads a part; utility types are built from both.

Predict first

Before you read: predict

ExamHall's admin form picks an input widget from each field's value type. A score field may hold a typed text or a number. This file is compiled with tsc, strict: true:

On which lines does tsc report an error?

Code for this question
type Widget<T> = T extends string ? "text-box" : "number-box";type ScoreField = string | number; const w1: Widget<ScoreField> = "text-box";     // line 4const w2: Widget<ScoreField> = "number-box";   // line 5
Pick one

The 30-second version

A mapped type writes one property per key; over keyof T it copies T, and a mapping can add or strike ? and readonly. A conditional type picks a branch by assignability, and on a bare type parameter it runs once per union member. infer names a part to pull out. Partial, ReturnType and their kin are ordinary types.

A mapped type loops over keys; a conditional type tests, per union member; infer reads a part; utility types are built from both.

How it works

Step 1 of 12

A mapped type loops over keys. { [K in Keys]: V } makes one property for each member of the key union; Record<K, V> is exactly that loop.
In the kitchen
  • one tray card printed for each seat number on a list → a mapped type over a key union (step 1)

Analogy: An airline caterer loading meal trays from a flight's passenger list (flight catering)

The caterer's unit prepares one tray per seat from the list the airline sends.

At the caterer's In TypeScript
one tray card printed for each seat number on a list a mapped type over a key union (step 1)
each booking line copied onto its card, the booking's own notes included a mapping over keyof T (steps 2, 4)
a stamp marking every card "may decline" or "do not swap", or striking that mark ?, readonly, -?, -readonly (step 3)
"vegetarian code? vegetarian tray : standard tray", applied seat by seat a distributive conditional type (step 8)
one question put to the whole row at once the bracketed test (step 9)
the loader reading the meal code off each booking whose service line fits the pattern infer (step 11)
a meal voucher exchanged desk after desk until a tray comes out Awaited (step 12)

In real terms: the compiler computes a new type from old ones: a mapped type by visiting keys, a conditional type by checking assignability, both before any code runs.

Breaks down when: the caterer handles real trays on the day. Every type here is gone before the code runs (ts-runtime-boundary-validation): no mapped type copies an object.

Contrasting case: a staff canteen with one menu for everyone. Nothing is prepared per seat, so there is no list to loop over and nothing to test.

Examples, run for real

4.1 ExamHall EXH-4570: a hand-written patch type drifts

ExamHall's admin edit form sends a patch of an exam's editable fields. Its type was written by hand. A release narrowed Exam.locale to the three languages the papers are printed in, and the form's type kept string (EXH-4570). MR !4870 derived the patch from Exam:

type Locale = "en" | "fr" | "ar";
interface Exam { readonly id: string; title: string; durationMin: number; locale: Locale; passMark: number }

interface ExamPatch { title?: string; durationMin?: number; locale?: string }       // before MR !4870
type ExamPatch = Partial<Pick<Exam, "title" | "durationMin" | "locale">>;          // MR !4870

const relocale: ExamPatch = { locale: "de" };
EXH-4570 hand-written ExamPatch (locale?: string) · tsc: no diagnostics · emitted JS reads HEADINGS["de"] -> undefined
MR !4870 ExamPatch = Partial<Pick<Exam, …>> · tsc: TS2322 at the { locale: "de" } line · { title: "Mock 3" } compiles in both

The hand-written type and Exam were two definitions, and nothing compared them. Pick reads the three properties' types out of Exam and Partial makes each optional, so the next change to Exam reaches the form (steps 2–3).

4.2 Ticketing: page props read off the loader

async function getOrder(ref: string) {
  return { ref, seats: 2, section: "B" };
}
type OrderProps = Awaited<ReturnType<typeof getOrder>>;
4.2 ReturnType<typeof getOrder>: Promise<{ ref: string; seats: number; section: string; }> · loaded.seats: TS2339
4.2 Awaited<ReturnType<typeof getOrder>>: { ref: string; seats: number; section: string; } · props.seats, props.section: no diagnostics

ReturnType of an async function is the promise, so reading seats from it fails; Awaited unwraps it, and a field the loader adds appears in the props without an edit (steps 11–12).

Picture it

Concept illustration for this lesson.
An illustration to help it stick. The mechanism is in the sections above.

Try it

Answer first, then check. Nothing is saved.

Question 1 of 7

A box office holds seats while a buyer pays. Two helper types copy a hold, and the file is compiled with strict: true:

On which lines does tsc report an error?

Code for this question
interface Hold { readonly seatId: string; note?: string } type Mirror<T> = { [K in keyof T]: T[K] };type Listed = { [K in "seatId" | "note"]: Hold[K] }; const a: Mirror<Hold> = { seatId: "A-12" };   // line 6a.seatId = "A-13";                            // line 7const b: Listed = { seatId: "A-12" };         // line 8b.seatId = "A-13";                            // line 9
Pick one

Question 2 of 7

A payments ledger types its status checks. Chargebacks have not been added to Status yet. Compiled with strict: true:

What type does tsc give r?

Code for this question
type Status = "pending" | "captured" | "refunded";type Disputed = Extract<Status, "chargeback">; type IsPending<T> = T extends "pending" ? true : false;type AllPending<T> = [T] extends ["pending"] ? true : false; declare const r: [IsPending<Disputed>, AllPending<Disputed>];
Pick one

Question 3 of 7

A chat app sends three kinds of message and pulls out the type of a text message's body. Compiled with strict: true:

What type does tsc give b?

Code for this question
type Msg =  | { kind: "text"; body: string }  | { kind: "image"; url: string }  | { kind: "poll"; body: string[] }; type TextBody<T> = T extends { kind: "text"; body: infer B } ? B : never; declare const b: TextBody<Msg>;
Pick one

Question 4 of 7

A podcast team wants to copy four of TypeScript's utility types into their own types.ts, so they can read and change them: NonNullable<T> drops null and undefined, NoInfer<T> is T but stops a call from inferring T from that spot, Parameters<T> gives a function's parameter list as a tuple, and Omit<T, K> drops the keys K from T. In TypeScript 6.0.3, one of the four is declared in the lib files with the keyword intrinsic, with no body to copy: the compiler supplies it.

Which one?

Pick one

Question 5 of 7

The interviewer says: "Our booking screens use Partial and Pick everywhere. Show me you could write them yourself, with no utility type inside." You have 10 minutes and a plain editor, and you say what you will check first.

Write MyPartial<T> and MyPick<T, K>.

Question 6 of 7

A payments ledger hands its request handlers the ledger and a batch of entries, both behind Readonly so that handlers cannot change them. Compiled with strict: true:

On which lines does tsc report an error?

Code for this question
interface Entry { id: string; amount: number }interface Ledger { owner: string; entries: Entry[] }declare const ledger: Readonly<Ledger>;declare const batch: Readonly<Entry[]>;ledger.entries[0] = { id: "e1", amount: 0 };   // line 5batch[0] = { id: "e1", amount: 0 };            // line 6batch[0].amount = 0;                           // line 7ledger.entries = [];                           // line 8
Pick one

Question 7 of 7

A streaming app's SDK returns query builders: each is a thenable, not a Promise. Compiled with strict: true:

On which lines does tsc report an error?

Code for this question
interface Episode { id: string; durationSec: number }declare function episodeQuery(id: string): PromiseLike<Episode>;type Once<T> = T extends Promise<infer U> ? U : T;declare const a: Once<ReturnType<typeof episodeQuery>>;declare const b: Awaited<ReturnType<typeof episodeQuery>>; a.durationSec;   // line 7b.durationSec;   // line 8
Pick one

Go deeper

Pro tips
  1. Stop when the type gets clever: deep recursion is a build failure. tsc gives up on a type that instantiates too deeply with TS2589, and the error lands in your callers' builds. Prefer a plain written-out type when a recursive one only saves a few lines. When: someone proposes a recursive utility type for a shared package. Verify (RAN, tips.mjs):
    tip 1 Countdown<40>: no diagnostics · Countdown<1200>: TS2589 (type instantiation is excessively deep)
  2. Test a hand-written type for exact equality, not assignability. readonly does not change assignability, so two checks in both directions pass where the types differ; the Same<X, Y> pattern catches it. When: replacing a utility type with your own, or checking a mapped type's modifiers. Verify (RAN, tips.mjs):
    tip 2 Seat (readonly row) and LooseSeat: assignable both ways (lines 5, 6: no diagnostics) · Same<Seat, LooseSeat> = true: TS2322 (not the same type)
Source

Quotes are checked verbatim by quote-check.mjs. Every graded fact is executed, so no item is keyed on these pages; they back the teaching. Valid as of 2026-10-16.

  • TypeScript Handbook, Mapped Types (the curriculum row's first source), https://www.typescriptlang.org/docs/handbook/2/mapped-types.html (ts-mapped-types.txt): "Mapped types build on the syntax for index signatures, which are used to declare the types of properties which have not been declared ahead of time" (steps 1–2); "There are two additional modifiers which can be applied during mapping: readonly and ? which affect mutability and optionality respectively." and "You can remove or add these modifiers by prefixing with - or +. If you don’t add a prefix, then + is assumed." (step 3).
  • TypeScript Handbook, Conditional Types (the row's second source), https://www.typescriptlang.org/docs/handbook/2/conditional-types.html (ts-conditional-types.txt): "When the type on the left of the extends is assignable to the one on the right, then you’ll get the type in the first branch (the “true” branch); otherwise you’ll get the type in the latter branch (the “false” branch)." (step 7); "When conditional types act on a generic type, they become distributive when given a union type." and "If we plug a union type into ToArray, then the conditional type will be applied to each member of that union." (step 8); "To avoid that behavior, you can surround each side of the extends keyword with square brackets." (step 9); "Conditional types provide us with a way to infer from types we compare against in the true branch using the infer keyword." (step 11).
  • TypeScript Handbook, Utility Types (the row's third source), https://www.typescriptlang.org/docs/handbook/utility-types.html (ts-utility-types.txt): Readonly "Constructs a type with all properties of Type set to readonly, meaning the properties of the constructed type cannot be reassigned." (step 5); Exclude "Constructs a type by excluding from UnionType all union members that are assignable to ExcludedMembers." (step 10); Awaited "This type is meant to model operations like await in async functions, or the .then() method on Promises - specifically, the way that they recursively unwrap Promises." (step 12).
  • TypeScript 6.0.3's lib.es5.d.ts (the compiler's own file, not a fetched page) holds the declarations step 6 rests on, the intrinsic ones included.

Checked against the tools and docs as of 2026-10-16.