Type operators
Every utility type from the last chapter is built from the same few parts: keyof, conditional types, infer, and mapped types. This chapter covers the parts one at a time. At the end you rebuild those five utility types yourself.
Two worlds: types are a small language of their own
Before taking a utility type apart, set up one distinction. Every line of TypeScript you write lives in two places at once: the code that runs, and the types the compiler checks.
Opening one up
In the last chapter you used Partial and Pick without asking how they work. Open one up and there is nothing unusual inside: something that reads keys, something that reads a type, something that makes a decision, something that loops. Put those together and the type system becomes a language you can program in. It takes types as input, produces types as output, and runs only while the code is being compiled.
Most JavaScript operations you already know have a counterpart that works on types. This table is the map of the chapter: each row is one section.
| Values (at run time, works on data) | Types (at compile time, works on types) | Section |
|---|---|---|
Object.keys(order) | keyof Order | §02 |
order["size"] | Order["size"] | §03 |
cond ? a : b | T extends U ? X : Y | §04 |
Destructuring: const { x } = obj | infer captures a part | §05 |
arr.map(fn) | { [K in keyof T]: … } | §06 |
Building a string: `on${name}` | Template literal type: `on${K}` | §06 |
None of this exists at run time
Everything in this chapter happens while tsc checks your code. When it finishes, all of it is removed. This is type erasure, the same rule as in chapter 00. So a complicated type costs you compile time and readability, but not one byte of the JavaScript you ship.
keyof and typeof: read the keys, read a value's type
The first two parts. One collects the keys of a type. The other takes the type of an existing value and brings it into the type world.
keyof T · collect the keys
Object.keys(order) gives you an array of strings when the program runs. keyof Order gives you a union of string literal types while the program is compiled. Every member of that union is a key that really exists, which is why "cup" is rejected.typeof x · read the type of a value
typeof first to get the type of the object, then keyof to get its keys. Add a drink to menu and DrinkName gains a member with no other edit. The typeof inside ReturnType<typeof makeOrder> in the last chapter is this same operator.Two different operators share the name typeof
The JavaScript typeof runs at run time and returns one of eight strings, such as "string" or "object" (chapter 03 used it for narrowing). The TypeScript typeof appears only in a type position — after the = of a type declaration, or after the : of an annotation — and it is removed when the code is compiled.
Judge by position, not by the word: if (typeof x === "string") is the JavaScript one, and type M = typeof menu is the TypeScript one.
Indexed access: T[K] reads a property type
The third part. You have the keys; now use one to read the type stored under it.
Order[keyof Order] shows that a union of types collapses when one member already covers another: Size is a union of strings, and string is in the union too, so Size is absorbed. Second, T[number] is the standard way to read the element type of an array. Combined with as const, which turns the array into a readonly tuple of literal types, one list of data also becomes the list of allowed values: edit it in one place and both sides follow.Three parts are now in place: keyof reads keys, typeof reads the type of a value, and T[K] reads the type of a property. Everything later in the chapter is built on these.
Conditional types and distribution
The fourth part, and the most important one in this chapter. First the conditional type itself. Then what happens when the type being checked is a union.
extends here means "is assignable to" — the same compatibility check as in chapter 04, now used as the condition of a ternary. If the check holds, the type is X; otherwise it is Y.On its own that is not much. The important behavior appears when the checked type is a union. The last chapter said that Exclude is defined in a single line, and this is that line. But how can a ternary remove members from a union?
extends is a naked type parameter — a bare T, as it is here. So the four members line up on the left and go through the check separately.This behavior is called a distributive conditional type, and it has one condition that you have to know: it happens only when the type on the left of extends is a naked type parameter — a bare T, with nothing wrapped around it. When it happens and T is A | B, the compiler rewrites the whole conditional type as (A extends U ? X : Y) | (B extends U ? X : Y). That is the entire rule. Without it, no one can predict what a conditional type will return.
Distribution needs a naked T. Wrap it and it stops
This is the design, not a defect. When you want each member handled separately, as Exclude does, use a naked T. When you want the union judged as a single type, wrap both sides in a one-element tuple: [T] extends [U]. Wrapping only one side changes what you are comparing, so wrap both.
Two results that surprise people, and the same rule explains both
never is the union with no members. Distributing over it runs the check zero times, so the result is never. It is not a special case in the compiler; there is simply nothing to distribute over. And boolean is not a single type: it is true | false, so a distributive conditional type runs twice over it. Remember these two and most confusing conditional-type results stop being confusing.
infer: capture a type while matching
The fifth part. A conditional type answers whether a type has a certain shape. infer also hands you the piece inside that shape.
T has the shape Promise<something>, call that something U and return U. infer declares a type variable inside the pattern after extends, and the compiler fills it in while matching. It is destructuring, done on types. The variable is available in the true branch only.ReturnType from the last chapter: if T is a function, capture its return type. TypeScript also allows a constraint on the variable (infer U extends …), which this chapter does not use.Mapped types: a loop over the keys
The last group of parts: the loop that rebuilds a type key by key, the modifiers you can add or remove, and the as clause that renames keys.
[K in keyof T] loops over the keys from §02; ? is a modifier that makes each property optional; T[K] is the indexed access from §03, which copies the original property type. That is the whole of Partial.? added before the colon. That is the entire definition of Partial.A mapped type over keyof T copies the modifiers you did not change
Written in the form { [K in keyof T]: … }, a mapped type does more than build a new object type. It keeps the ? and readonly of the source wherever it does not change them, and it passes an array through as an array and a tuple through as a tuple. Such a mapped type is called homomorphic. This is the reason -? and -readonly have to exist: the modifiers would otherwise be copied and there would be no way to drop them.
as + template literal · rename the keys as well (TS 4.1)
as clause gives the new key name. A template literal type builds that name from pieces, and Capitalize raises the first letter. The string & K is required, not decoration: keyof T can also contain number and symbol, while Capitalize<S> only accepts S extends string. Leave it out and the compiler reports Type 'K' does not satisfy the constraint 'string'. Mapping a key to never instead removes it, which is how a mapped type can filter keys as well as rename them. Both as and template literal types arrived in TypeScript 4.1.Rebuild the five utility types yourself
Chapter goalAll the parts are covered. On the left is the definition from lib.es5.d.ts; on the right is the version you would write. Each one uses only the parts from this chapter.
MyPartial · make every property optionalParts: mapped type + ? + T[K]
MyReadonly · make every property readonlyParts: mapped type + readonly + T[K]
MyPick · keep only the listed keysParts: mapped type + constraint + T[K]
Notice what changed: the loop runs over K, the keys the caller asked for, not over keyof T. And K extends keyof T — a generic constraint from chapter 05 — makes sure every requested key really exists. That is why a misspelled key is reported by Pick and not by Omit: Omit constrains its keys to keyof any instead. The open question from the last chapter is answered.
MyExclude · filter a unionParts: conditional type + distribution + never
MyReturnType · capture a function's return typeParts: conditional type + infer
The only difference is the fallback branch: the library uses any, and this version uses never. The constraint already guarantees that T is a function, so under normal use that branch is never taken. Both are correct; never is the stricter choice.
You can now read the library
Five utility types, rewritten by hand, and your versions are nearly word for word the same as the library's. There are only a few parts; the rest is combination. In VS Code, hold Cmd or Ctrl and click Partial to jump into lib.es5.d.ts. The file that looked unreadable one chapter ago now reads as plain code. And when the built-in types are not enough — a deep Readonly, a strict Omit — you can write your own. One of the labs below is exactly that.
Being able to write it is not a reason to
Type-level code follows the same rule as ordinary code: if a reader can understand it at a glance, do not write it as three nested conditionals. If an interface says it clearly, do not reach for a conditional type. A type that takes a colleague ten seconds of hovering to understand is a cost, not an achievement. The final chapter, on how to think about types, returns to where that line sits.
Hands-on tasks
Four tasks, getting harder: turn distribution off, build getters with a template literal type, unwrap nested Promises, and build a utility type the library does not ship.
Chapter quiz
Eight questions. Distribution and infer are where most mistakes happen; if you are unsure, replay the visualization in §04 one step at a time.
Order has six fields: id, drink, size, sugar, toppings, internalNote. What is keyof Order?
Which of these four uses of typeof belongs to the type world?
After const t = ["boba", "coconut jelly"] as const;, what is (typeof t)[number]?
How does the compiler arrive at Exclude<"a" | "b" | "c", "b">?
Given type W<T> = [T] extends [string] ? "pure" : "mixed";, what is W<"a" | 1>?
What does the infer keyword do?
In a mapped type, which symbol goes before ? to remove the optional modifier, so that every property becomes required? Type the symbol: ____
Which pieces does the hand-written MyPick<T, K extends keyof T> = { [P in K]: T[P] } use? (choose all that apply)
- Types are a small language of their own: types in, types out, run only while the code is compiled, and removed afterwards. Most value-level operations have a type-level counterpart.
- Three ways to read:
keyof Tgives the union of key names,typeof xgives the type of a value, andT[K]gives the type of a property (T[number]for the element type of an array). T extends U ? X : Yis the ternary of the type world. It distributes over a union only when the checked type is a naked type parameter:A | Bbecomes(A extends U ? X : Y) | (B extends U ? X : Y). A member that becomesneverdisappears, becauseneveris the empty union. That is all ofExclude.- Wrapping both sides —
[T] extends [U]— turns distribution off. Two follow-on facts:neverdistributes over nothing and yieldsnever, andbooleanistrue | false, so it distributes twice. infercaptures a type out of a matched shape. A mapped type{ [K in keyof T]: … }rebuilds a type key by key, copies the modifiers it does not change, and accepts?,-?,readonlyand-readonly. Anasclause with a template literal type renames keys, and mapping a key toneverremoves it (TS 4.1).- You have rewritten all five utility types from the last chapter by hand. Keep the restraint that goes with it: readability comes before cleverness, and a recursive type still has a depth limit (
ts(2589)).