Handbooks / TypeScript / Chapter 5

Utility and Mapped Types

7 pages · ~5 min✓ Reviewed

Builds on Generics.

TypeScriptPage 1

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

UtilityEffect
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
  • Pick and Omit are built from mapped types

3. Shallow Only

  • Partial<User> does not make nested objects optional
  • Readonly<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';  // allowed

only the top level is readonly

Compile time vs runtime

Readonly<T> is compile-time only. Object.freeze is the runtime guard.

TypeScriptPage 2

Utility Types in Everyday Use

1. Reshaping User

  • Partial<User> as patch: any subset of User fields
  • Omit<User, 'password'> hides sensitive fields
  • Pick<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>
Keysfixed unionany string
Missing keyerror, all requiredallowed
Shapemapped typeindex signature
  • With noUncheckedIndexedAccess, a lookup returns P | undefined
  • Narrow it first: if (rec[k]) ...

3. The Omit Trap

  • Omit<T, K> has K 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.

TypeScriptPage 3

Function Types: ReturnType, Parameters, infer

1. Extracting Types

  • ReturnType<typeof fn> extracts the return type
  • Pass typeof fn, not fn: ReturnType<fn> is an error
  • Parameters<typeof fn> gives the arguments as a tuple
  • Parameters<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 R declares a type variable inside extends
  • 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

TypeScriptPage 4

Mapped Types and Conditional Types

1. Mapped Types

  • { [K in keyof T]: T[K] } loops over the keys of T
  • Partial<T> = { [K in keyof T]?: T[K] }
  • -? removes optional (Required<T>); -readonly strips readonly
  • as renames keys; mapping a key to never drops 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 : Y picks X or Y
  • A naked T distributes over a union
  • type ToArray<T> = T extends any ? T[] : never
  • ToArray<string | number> gives string[] | number[]
FormUnion inputChecks
T extends Udistributeseach member
[T] extends [U]no distributionwhole type
Testing for never

never is an empty union, so a distributive conditional yields never. Use [T] extends [never].

TypeScriptPage 5

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
  • red stays number[], 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: Tsatisfies T
Validatesyesyes
c.red typenumber[] | stringnumber[]
Typo / missing keyerrorerror
Exact-key autocompletelostkept
  • Typos and missing keys fail at the declaration
  • Add as const for literal, readonly types

3. Common Mistakes

  • Partial, Readonly, Required are shallow: use recursive DeepPartial<T>
  • Omit on 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.

TypeScriptPage 6

Cheat Sheet: Utility and Advanced Types

1. Utility Types

TypeDoes
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
  • Omit never checks keys: a typo omits nothing

2. Function Extraction

  • Pass typeof f, not f
  • Awaited unwraps 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 strip readonly and ?
  • T extends U ? X : Y; infer R captures a type
  • Naked T distributes over unions
  • [T] extends [U] does not distribute
  • satisfies validates 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>.

TypeScriptPage 7

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 .map doesn't exist on string