Mapped Types and Key Remapping

Advanced
12 min

Mapped Types and Key Remapping

A mapped type creates a new object type by looping over the keys of an existing one and deciding, per key, what the property should look like. It is how Partial, Readonly, Pick and Record are implemented, and it is the tool you reach for when a type must stay in sync with another. This lesson covers the basic syntax, the modifiers, key remapping with as, and the recursive forms used for deep transformations.

Basic Mapped Types

typescript
interface Settings { theme: string; fontSize: number; autosave: boolean } type Flags<T> = { [K in keyof T]: boolean }; type SettingFlags = Flags<Settings>; // { theme: boolean; fontSize: boolean; autosave: boolean }

[K in keyof T] iterates over each property name, and the part after the colon is the new property type, which may use K and T[K]. Any union of string, number or symbol literals can drive the loop, not just keyof:

typescript
type Weekday = "mon" | "tue" | "wed"; type Schedule = { [D in Weekday]: string[] }; // { mon: string[]; tue: string[]; wed: string[] }

This is exactly how Record<K, V> is defined: { [P in K]: V }.

Modifiers: readonly and ?

A mapped type can add or remove the readonly and optional modifiers with + and - (the + is implied when omitted):

typescript
type MyPartial<T> = { [K in keyof T]?: T[K] }; type MyRequired<T> = { [K in keyof T]-?: T[K] }; type MyReadonly<T> = { readonly [K in keyof T]: T[K] }; type Mutable<T> = { -readonly [K in keyof T]: T[K] }; interface Config { readonly host: string; port?: number } type Editable = Mutable<MyRequired<Config>>; // { host: string; port: number }

When a mapped type iterates over keyof T of a type parameter, it is homomorphic: it preserves the original modifiers unless you change them, and it maps arrays and tuples element-wise instead of turning them into plain objects. Partial<[string, number]> is therefore [string?, number?], not an object with keys "0" and "1".

Key Remapping With as

TypeScript 4.1 added an as clause that computes a new key for each property. Combined with template literal types, it renames keys; combined with never, it filters them out:

typescript
interface Product { id: number; title: string; price: number; save(): void } // Rename: on + capitalised key type Handlers<T> = { [K in keyof T as `on${Capitalize<string & K>}Change`]: (value: T[K]) => void; }; // Filter: drop methods, keep data properties type DataOnly<T> = { [K in keyof T as T[K] extends (...args: any[]) => any ? never : K]: T[K]; }; type ProductHandlers = Handlers<DataOnly<Product>>; // { onIdChange: (value: number) => void; onTitleChange: ...; onPriceChange: ... }

string & K is required because keyof T may contain number | symbol, which template literals cannot embed. Returning never from the as clause removes the property entirely, which is how Omit can be written: { [K in keyof T as K extends Keys ? never : K]: T[K] }.

Mapping a Union to an Object

Key remapping can also turn a discriminated union into a lookup object, using the discriminant as the key:

typescript
type Event = | { type: "click"; x: number; y: number } | { type: "keypress"; key: string }; type EventMap = { [E in Event as E["type"]]: E }; // { click: { type: "click"; x: number; y: number }; keypress: { type: "keypress"; key: string } } function on<K extends keyof EventMap>(type: K, handler: (e: EventMap[K]) => void) { /* ... */ } on("keypress", (e) => e.key); // e: { type: "keypress"; key: string }

Here E iterates over each union member rather than over keys, and E["type"] supplies the new key.

Recursive Mapped Types

Combining a mapped type with a conditional type produces deep transformations. The recursion stops at primitives and functions:

typescript
type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? T[K] extends (...args: any[]) => any ? T[K] : DeepPartial<T[K]> : T[K]; }; interface AppConfig { server: { host: string; port: number }; debug: boolean } const override: DeepPartial<AppConfig> = { server: { port: 9000 } }; // OK

DeepReadonly and DeepRequired follow the same shape. Date, Map and class instances are object too, so exclude them (T[K] extends Date ? T[K] : ...) when they must stay intact.

With these pieces you can read the standard library: Pick<T, K> is { [P in K]: T[P] }, Record<K, V> is { [P in K]: V }, and Omit<T, K> is Pick<T, Exclude<keyof T, K>>.

Common mistakes

  • Forgetting string & K inside a template literal key, which fails on symbol keys.
  • Writing { [K in T]: ... } where T is an object type; the loop variable needs a union of keys, so use keyof T.
  • Recursing into every object, which mangles Date and class instances.
Quick Quiz
Question 1 of 3

What does `{ [K in keyof T]-?: T[K] }` produce?

Key Takeaways

  • { [K in keyof T]: ... } loops over property names to build a new object type from an old one.
  • +/- with readonly and ? add or strip modifiers; this is how Partial, Required and Readonly work.
  • Homomorphic mapped types keep existing modifiers and map arrays and tuples element-wise.
  • The as clause renames keys (often with template literals) and drops them by returning never.
  • Recursive mapped types with conditional guards enable DeepPartial and similar deep transformations.

Next lesson: Template Literal Types — build and parse string literal types with type-level string interpolation.

Mapped Types and Key Remapping - TypeScript | CodeYourCraft | CodeYourCraft