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.
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:
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 }.
A mapped type can add or remove the readonly and optional modifiers with + and - (the + is implied when omitted):
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".
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:
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] }.
Key remapping can also turn a discriminated union into a lookup object, using the discriminant as the key:
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.
Combining a mapped type with a conditional type produces deep transformations. The recursion stops at primitives and functions:
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 } }; // OKDeepReadonly 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>>.
string & K inside a template literal key, which fails on symbol keys.{ [K in T]: ... } where T is an object type; the loop variable needs a union of keys, so use keyof T.object, which mangles Date and class instances.What does `{ [K in keyof T]-?: T[K] }` produce?
{ [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.as clause renames keys (often with template literals) and drops them by returning never.DeepPartial and similar deep transformations.Next lesson: Template Literal Types — build and parse string literal types with type-level string interpolation.