Handbooks / TypeScript / Chapter 5
Utility and Mapped Types
7 pages · ~5 min✓ Reviewed
Builds on Generics.
Type Transformation Mental Model
1. Types as Functions
- Utility type = generic type-level function: type in, new type out
- Erased at compile time: zero runtime cost
2. The Core Set
| Utility | Effect |
|---|---|
Partial<T> | every property optional |
Required<T> | strips ?, all required |
Readonly<T> | every property readonly |
Pick<T, K> | keeps only keys K |
Omit<T, K> | drops keys K |
PickandOmitare built from mapped types
3. Shallow Only
Partial<User>does not make nested objects optionalReadonly<T>does not freeze nested properties
typescript
type User = { name: string; address: { city: string } };
const u: Readonly<User> = { name: 'A', address: { city: 'X' } };
u.name = 'B'; // error
u.address.city = 'Y'; // allowedonly the top level is readonly
Compile time vs runtime
Readonly<T> is compile-time only. Object.freeze is the runtime guard.
Utility Types in Everyday Use
1. Reshaping User
Partial<User>aspatch: any subset of User fieldsOmit<User, 'password'>hides sensitive fieldsPick<User, 'id' | 'name'>whitelists instead
typescript
function update(id: string, patch: Partial<User>) {} type PublicUser = Omit<User, 'password'> type Card = Pick<User, 'id' | 'name'>
2. Record<K, V>
Record<'admin' | 'user', P> | Record<string, P> | |
|---|---|---|
| Keys | fixed union | any string |
| Missing key | error, all required | allowed |
| Shape | mapped type | index signature |
- With noUncheckedIndexedAccess, a lookup returns
P | undefined - Narrow it first:
if (rec[k]) ...
3. The Omit Trap
Omit<T, K>hasK extends keyof any: keys are not checked- A typo compiles and omits nothing
typescript
type Oops = Omit<User, 'pasword'> // no error: password is still in Oops
Omit vs Pick
Pick checks its keys against keyof T; Omit does not. Re-read the resulting type after every Omit.
Function Types: ReturnType, Parameters, infer
1. Extracting Types
ReturnType<typeof fn>extracts the return type- Pass
typeof fn, notfn:ReturnType<fn>is an error Parameters<typeof fn>gives the arguments as a tupleParameters<typeof fn>[0]is the first argument
- Async functions return
Promise<T>, so unwrap with Awaited<...>
typescript
async function load(id: string) { return user } type R = ReturnType<typeof load> // Promise<User> type U = Awaited<R> // User
2. infer
infer Rdeclares a type variable insideextends- Legal only in the extends clause
- It is pattern matching on the type's shape
typescript
type MyReturn<T> = T extends (...args: any[]) => infer R ? R : never
Remember
ReturnType is just a conditional type with infer
Mapped Types and Conditional Types
1. Mapped Types
{ [K in keyof T]: T[K] }loops over the keys of TPartial<T>={ [K in keyof T]?: T[K] }-?removes optional (Required<T>);-readonlystrips readonly- as renames keys; mapping a key to
neverdrops it
typescript
type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K]
}renames name to getName
2. Conditional Types
T extends U ? X : Ypicks X or Y- A naked
Tdistributes over a union type ToArray<T> = T extends any ? T[] : neverToArray<string | number>givesstring[] | number[]
| Form | Union input | Checks |
|---|---|---|
T extends U | distributes | each member |
[T] extends [U] | no distribution | whole type |
Testing for never
never is an empty union, so a distributive conditional yields never. Use [T] extends [never].
satisfies and Common Mistakes
1. What satisfies Does
- satisfies (TS 4.9) checks a value against a type
- It does not widen the value's inferred type
redstaysnumber[], not the union
typescript
const c = { red: [255, 0, 0], green: "#0f0", } satisfies Record<string, number[] | string>; c.red.map(n => n / 2); // number[] c.green.toUpperCase(); // string
each property keeps its own type
2. Annotation vs satisfies
| const c: T | satisfies T | |
|---|---|---|
| Validates | yes | yes |
c.red type | number[] | string | number[] |
| Typo / missing key | error | error |
| Exact-key autocomplete | lost | kept |
- Typos and missing keys fail at the declaration
- Add
as constfor literal, readonly types
3. Common Mistakes
Partial,Readonly,Requiredare shallow: use recursiveDeepPartial<T>Omiton a union collapses its variants: distribute it
typescript
type DeepPartial<T> = {
[K in keyof T]?: DeepPartial<T[K]>;
};
type DistOmit<T, K extends keyof any> =
T extends any ? Omit<T, K> : never;Remember
Annotation widens; satisfies validates and keeps the exact type.
Cheat Sheet: Utility and Advanced Types
1. Utility Types
| Type | Does |
|---|---|
Partial<T> | all optional |
Required<T> | all required |
Readonly<T> | all readonly |
Pick<T, 'a' | 'b'> | keeps those keys |
Omit<T, 'a'> | drops keys |
Record<K, V> | object from key union |
- Modifiers are all shallow
Omitnever checks keys: a typo omits nothing
2. Function Extraction
- Pass
typeof f, notf Awaitedunwraps the Promise
typescript
type R = ReturnType<typeof f> type P = Parameters<typeof f> type A = Awaited<ReturnType<typeof g>>
3. Mapped, Conditional, satisfies
+/-add or stripreadonlyand?T extends U ? X : Y;infer Rcaptures a type- Naked
Tdistributes over unions [T] extends [U]does not distributesatisfiesvalidates without widening
typescript
type M<T> = {
-readonly [K in keyof T as `get${Capitalize<K & string>}`]-?: T[K]
}mapped template
Shallow
Nested objects stay untouched; write a recursive DeepPartial<T>.
Check yourself
type A = Omit<{ id: number; name: string }, 'nme'> compiles. What is A?
- Still
{ id: number; name: string } - Omit does not check that K is a key of T, so the typo drops nothing
type A<T> = T extends unknown ? T[] : never and type B<T> = [T] extends [unknown] ? T[] : never. What are A and B for string | number?
- A gives
string[] | number[], because a naked T distributes over the union - B gives
(string | number)[], because the tuple wrap stops distribution
const c = { red: [255,0,0] } satisfies Record<string, number[] | string>. Does c.red.map(...) compile? What if you write const c: Record<string, number[] | string> = ... instead?
- With satisfies, yes: red stays
number[] - With the annotation, no: red widens to
number[] | string, and.mapdoesn't exist on string