Mapped types
Walk every key of an object type and rebuild it. Add optionality, drop readonly, rename keys.
You have a User type with five fields, and you need a version where every field is optional. You could write a second type by hand. But the moment User gains a sixth field, the hand-written version is wrong. A mapped type writes the second one for you, and keeps it in sync forever.
The mapped-type shape
A mapped type walks every key of a source type and builds a new type, key by key. The syntax looks like an index signature but with keyof T driving it.
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
type User = { id: number; name: string; email: string };
type DraftUser = MyPartial<User>;
// { id?: number; name?: string; email?: string }Read it as a loop. keyof T produces a union of all the keys ("id" | "name" | "email"). [K in keyof T] means for each K in that union. The right side, T[K], says the type of T at key K. The ? marker after the ] makes every resulting field optional.
T[K] is called an indexed access type — a way to ask what type sits at this key? If User is { id: number; name: string }, then User["id"] is number. Combine that with the loop and you can rebuild any object type, transforming the values as you go.
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
type FrozenUser = MyReadonly<User>;
// { readonly id: number; readonly name: string; readonly email: string }Same loop, different modifier. The readonly keyword gets stamped onto every field of the result.
type R = { [K in keyof { a: 1; b: 2 }]: string } resolve to?Modifiers and the +/- prefix
There are two modifiers a mapped type can change: readonly and ? (optionality). By default, writing readonly adds it and writing ? adds it. To remove one, prefix with -.
// Strip optionality from every field
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
// Strip readonly from every field
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
type Locked = { readonly id: number; readonly name?: string };
type Open = Mutable<Locked>;
// { id: number; name?: string } — readonly gone, optionality keptThe minus prefix removes the marker if the source had it. Without a prefix, the modifier is added (or kept, since adding readonly to something that's already readonly is a no-op). You almost never need to write +readonly explicitly — that's the default.
readonly in TypeScript only blocks reassignment through that handle. const u: Readonly<User> still shares the same object — passing it to a non-readonly function can mutate it. The check is shallow and structural.
Key remapping with as
The mapped-type loop also lets you rename keys on the way through. The as clause sits right inside the brackets and replaces the key name with whatever you compute from K.
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type User = { id: number; name: string };
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string }Several things happen in one line. Capitalize<...> is a built-in that uppercases the first letter — covered next lesson. The backticks are template literal types, also next lesson. The string & K intersects K with string because keyof T can be string | number | symbol, and Capitalize only accepts strings. The result is getId and getName instead of id and name.
The other big use of as is filtering. If the new key resolves to never, that field disappears from the result.
// Keep only the keys whose value is a function
type FunctionKeys<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K];
};
type Mixed = { id: number; save(): void; load(): void };
type R = FunctionKeys<Mixed>;
// { save(): void; load(): void } — id droppedThe conditional inside as produces either K (keep this key) or never (drop this key). Mapped types treat a key of never as don't include this entry. So id, whose value is a number, gets remapped to never and vanishes from the result.
type R = { [K in keyof { a: 1; b: 2 } as K extends "a" ? K : never]: string }, what is R?Building Stringify
Tying the pieces together: write a type that turns every value of an object into string. The keys stay; the value types all become string.
type Stringify<T> = {
[K in keyof T]: string;
};
type User = { id: number; name: string; admin: boolean };
type UserAsStrings = Stringify<User>;
// { id: string; name: string; admin: string }The loop runs over every key, and the value side ignores T[K] entirely — it just hands back string. This is what makes mapped types feel like a programming language: the value side is an arbitrary type expression, and you can compute it however you want using T, K, and other utilities.
A more interesting variant flips primitives but leaves objects alone, recursing into them.
type DeepStringify<T> = {
[K in keyof T]: T[K] extends object
? DeepStringify<T[K]>
: string;
};
type Nested = { id: number; profile: { age: number; city: string } };
type R = DeepStringify<Nested>;
// { id: string; profile: { age: string; city: string } }For each value, the conditional asks is this an object? If yes, recurse. If no, replace with string. This pairs every concept so far — mapped types, conditional types, indexed access — into a single recursive shape. Recursion gets its own lesson; for now just notice how naturally the pieces fit together.
Mapped types replace the whole shape, including methods. Once you map an object type, things like class methods can lose their this typing. Mapped types are best on plain data shapes.
Try it yourself
{ id: number; name?: string } and produces { id?: number; name?: string } (every field optional). Which works?as clause inside a mapped type do?type Mutable<T> = { -readonly [K in keyof T]: T[K] }, what is Mutable<{ readonly a: number; b: string }>?