Siddhant Deval
Siddhant DevalAuthor
Senior Full-Stack Engineer·Aug 27, 2026·13 min read

Utility Types & Mapped Types: The Standard Library

The built-in utility types are not magic — they are one-liners built with mapped types. Reading their source and building your own reveals the full power of type-level transformation and eliminates the need to reach for `any` in complex object manipulation.

Utility Types & Mapped Types: The Standard Library

Types are a specification language — not an annotation layer. The built-in utility types are the most visible part of that language's standard library. But most developers treat them as magic incantations — Partial<T> makes everything optional, Readonly<T> makes everything readonly, and that's that. Read the source code. Partial<T> is three tokens: { [K in keyof T]?: T[K] }. Understanding the mechanism means you can compose new utilities, not just consume existing ones.

1. Mapped Types — The Mechanism

A mapped type iterates over the keys of a type and transforms each property. The syntax is deliberately similar to a for...in loop:
typescript
// The general form
type Mapped<T> = {
  [K in keyof T]: TransformedType
}
  • K is a type variable bound to each key of T in turn
  • keyof T produces the union of all keys of T as literal string types
  • The right side can reference T[K] (the type of the current property)

1.1 Reading the Source of Partial<T>

typescript
// TypeScript's actual definition (lib.es5.d.ts):
type Partial<T> = {
  [K in keyof T]?: T[K]
}

// What it produces for a concrete type:
interface User { name: string; email: string; age: number }

type PartialUser = Partial<User>
// Equivalent to:
// {
//   name?: string | undefined;
//   email?: string | undefined;
//   age?: number | undefined;
// }

1.2 The Full Utility Type Taxonomy

TypeSource ImplementationWhat It Does
Partial<T>{ [K in keyof T]?: T[K] }Makes all properties optional
Required<T>{ [K in keyof T]-?: T[K] }Removes optionality from all properties
Readonly<T>{ readonly [K in keyof T]: T[K] }Makes all properties readonly
Pick<T, K>{ [P in K]: T[P] }Keeps only the listed keys
Omit<T, K>Pick<T, Exclude<keyof T, K>>Removes the listed keys
Record<K, V>{ [P in K]: V }Creates an object type with keys K and values V
Exclude<T, U>T extends U ? never : TRemoves U from union T
Extract<T, U>T extends U ? T : neverKeeps only U from union T
NonNullable<T>T extends null | undefined ? never : TRemoves null and undefined
ReturnType<T>T extends (...args: any) => infer R ? R : anyExtracts the return type of a function
Parameters<T>T extends (...args: infer P) => any ? P : neverExtracts the parameter types as a tuple
Awaited<T>Recursive conditional (unwraps Promise chains)Unwraps Promise<T> recursively

2. Modifiers — +?, -?, +readonly, -readonly

Mapped types support modifier tokens to add or remove optionality and readonly-ness:
typescript
// Adding modifiers — `+` is implicit (you can omit it)
type AllOptional<T> = { [K in keyof T]+?: T[K] }   // same as { [K in keyof T]?: T[K] }
type AllReadonly<T> = { +readonly [K in keyof T]: T[K] }  // same as { readonly [K in keyof T]: T[K] }

// Removing modifiers — `-` explicitly strips the modifier from the source type
type Required<T>   = { [K in keyof T]-?: T[K] }    // strips optionality
type Mutable<T>    = { -readonly [K in keyof T]: T[K] }  // strips readonly
Crucial Requirement
The -? modifier is what makes Required<T> work. Without it, a plain mapped type preserves the ? modifier from the source — { [K in keyof T]: T[K] } produces a type with the same optionality as T. The -? strips it.
 2×2 grid diagram. Rows labeled 'Optional (?)' and 'Required (no ?)'. Columns labeled 'With modifier' and 'Without modifier'. Cells: top-left '+? (add optional)' shows name?: string; top-right '(no -?) preserves source' shows name?: string from optional source; bottom-left '-? (remove optional)' shows name: string; bottom-right '(no -?) preserves source' shows name: string from required source. An arrow below reads: 'Modifier math: -? strips optionality regardless of source; without it, the source modifier is preserved'. Caption: 'Mapped type modifiers are additive (+) or subtractive (-) — -? strips optionality from every property regardless of its state in the source type'.
Figure: 2×2 grid diagram. Rows labeled 'Optional (?)' and 'Required (no ?)'. Columns labeled 'With modifier' and 'Without modifier'. Cells: top-left '+? (add optional)' shows name?: string; top-right '(no -?) preserves source' shows name?: string from optional source; bottom-left '-? (remove optional)' shows name: string; bottom-right '(no -?) preserves source' shows name: string from required source. An arrow below reads: 'Modifier math: -? strips optionality regardless of source; without it, the source modifier is preserved'. Caption: 'Mapped type modifiers are additive (+) or subtractive (-) — -? strips optionality from every property regardless of its state in the source type'.

3. Key Remapping with as

You can rename or filter keys in a mapped type using the as clause:
typescript
// Rename all keys with a prefix
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}

interface State { count: number; name: string }

type StateGetters = Getters<State>
// {
//   getCount: () => number;
//   getName:  () => string;
// }

// Filter keys — map to `never` to remove them
type OmitFunctions<T> = {
  [K in keyof T as T[K] extends Function ? never : K]: T[K]
}
 Flow diagram. Left box: interface State { count: number; name: string }. Center box: mapped type with as \get${Capitalize<string & K>}`. Right box: result { getCount: () => number; getName: () => string }. Arrows show each key transformation: 'count'→ capitalize →'Count'→ prepend'get'→'getCount'. Caption: 'Key remapping with as` applies a type-level string transformation to each property name during mapped type iteration'.
Figure: Flow diagram. Left box: interface State { count: number; name: string }. Center box: mapped type with as \get${Capitalize<string & K>}`. Right box: result { getCount: () => number; getName: () => string }. Arrows show each key transformation: 'count'→ capitalize →'Count'→ prepend'get'→'getCount'. Caption: 'Key remapping with as` applies a type-level string transformation to each property name during mapped type iteration'.

4. Homomorphic vs Non-Homomorphic Mapped Types

This distinction determines whether a mapped type preserves modifiers from the source type:
typescript
// Homomorphic — iterates `keyof T` (the source type's own keys)
// Preserves readonly and ? modifiers from T
type Homomorphic<T> = { [K in keyof T]: T[K] }

// Non-homomorphic — iterates a fresh key union, not keyof T
// Does NOT preserve modifiers from T
type NonHomomorphic<K extends string, V> = { [P in K]: V }

// Example
interface Source { readonly name?: string }

type A = Homomorphic<Source>      // { readonly name?: string }  — modifiers preserved
type B = NonHomomorphic<'name', string> // { name: string }      — modifiers lost
Architectural Note
All of TypeScript's built-in utility types that iterate keyof T are homomorphic. Record<K, V> is non-homomorphic because it takes the keys as a fresh parameter, not from a source type's keyof.

5. Building Deep Utilities

Flat mapped types only transform the top level. To recurse into nested objects, combine mapped types with conditional types:
typescript
// ❌ Flat `Readonly` — only makes top-level properties readonly
type ShallowReadonly<T> = { readonly [K in keyof T]: T[K] }

// ✅ Deep `Readonly` — recurses into every nested object
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function
      ? T[K]                     // Don't wrap functions
      : DeepReadonly<T[K]>       // Recurse into objects
    : T[K]                       // Primitives — leave as-is
}

interface Config {
  server: { host: string; port: number }
  auth:   { secret: string; expiresIn: number }
}

type FrozenConfig = DeepReadonly<Config>
// {
//   readonly server: { readonly host: string; readonly port: number }
//   readonly auth:   { readonly secret: string; readonly expiresIn: number }
// }
 Tree diagram showing DeepReadonly<Config> recursion. Root node: Config. Two child nodes: server and auth. Each has a dashed DeepReadonly<> call wrapping it. Under server: leaf nodes host: string and port: number, both with a readonly tag. Under auth: leaf nodes secret: string and expiresIn: number, both with a readonly tag. A decision diamond at each branch node reads: 'Is T[K] an object (non-function)? → recurse : apply readonly to leaf'. Caption: 'DeepReadonly<T> combines a mapped type with a recursive conditional — the conditional selects between recursion and the base case at each property'.
Figure: Tree diagram showing DeepReadonly<Config> recursion. Root node: Config. Two child nodes: server and auth. Each has a dashed DeepReadonly<> call wrapping it. Under server: leaf nodes host: string and port: number, both with a readonly tag. Under auth: leaf nodes secret: string and expiresIn: number, both with a readonly tag. A decision diamond at each branch node reads: 'Is T[K] an object (non-function)? → recurse : apply readonly to leaf'. Caption: 'DeepReadonly<T> combines a mapped type with a recursive conditional — the conditional selects between recursion and the base case at each property'.

Summary

ConceptRule
Mapped type{ [K in keyof T]: Transform } — iterates keys and transforms each property
+? / -?Add or remove optionality; -? is what makes Required<T> work
+readonly / -readonlyAdd or remove the readonly modifier independently of optionality
Key remapping (as)Rename keys with a template literal or filter them by mapping to never
HomomorphicIterates keyof T — preserves source modifiers; all built-in utilities are homomorphic
Non-homomorphicIterates a fresh key union — does not preserve source modifiers
DeepReadonly<T>Requires a recursive conditional type; flat mapped types only transform one level

What's Next

In Part 5, we cover Conditional Types & infer — the type-level equivalent of if-else and pattern matching. They are what power ReturnType<T>, Parameters<T>, Awaited<T>, and every type-level algorithm in major TypeScript libraries.
Research & Synthesis Note

This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.

#TypeScript#Utility Types#Mapped Types#Type Transformation#Advanced TypeScript
Siddhant Deval

Written by Siddhant Deval

Senior Full-Stack Engineer building high-scale architectures, browser performance engineering systems, and SaaS platforms.