#Mapped types
A mapped type is a for…in loop that runs inside the type checker. { [P in K]: X } reads as "for every member P of the union K, create a property named P whose type is X". Most of the time K is keyof T, so you are looping over another type's keys, and X mentions T[P] (an indexed access type) to look up what that property's type was. The result is a brand-new object type; the original is never changed.
The runtime analogy is Object.fromEntries(Object.keys(obj).map((k) => [k, f(obj[k])])) — take every key, compute a new value, build a new object. A mapped type does the same thing to types, at compile time, and leaves nothing behind in the emitted JavaScript. Every utility type in How utility types work — Partial, Readonly, Pick, Record — is a one-line mapped type.
The mental model that makes them readable: picture the loop running once per key and write down the object it produces. That's what every walkthrough in this chapter does.
From my notes — "Mapped Types allow you to create new types by transforming existing types":
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
interface Person {
name: string;
age: number;
location: string;
}
type ReadOnlyPerson = { [P in keyof Person]: Readonly<Person[P]> };
const person: ReadOnlyPerson = { name: "Rohit", age: 30, location: "Toronto" };
person.age = 31; // compiles — nothing in ReadOnlyPerson is readonly
type T1 = Expect<Equal<ReadOnlyPerson, Person>>; // it's just Person againkeyof Personis the union"name" | "age" | "location", so the loop variablePtakes those three values one after another.P = "name": the property type isReadonly<Person["name"]>=Readonly<string>.Readonlyis itself a mapped type, and a mapped type applied to a primitive gives back the primitive unchanged — there are no keys to loop over. So the property isname: string.P = "age"givesReadonly<number>=number;P = "location"givesstring. The intermediate object is{ name: string; age: number; location: string }— identical toPerson, which is exactly what theT1assertion proves.- The key point:
readonlyis a property modifier, and in a mapped type it goes in front of the key:readonly [P in keyof Person]. Wrapping the value inReadonly<…>makes the value's own contents readonly (if it has any), but never the property that holds it. - So
person.age = 31compiles and would run fine. The type's name says "read-only"; the type itself says nothing of the sort.
interface Person {
name: string;
age: number;
location: string;
}
type ReadOnlyPerson = { readonly [P in keyof Person]: Person[P] };
// type ReadOnlyPerson = { readonly name: string; readonly age: number; readonly location: string }
const person: ReadOnlyPerson = { name: "Rohit", age: 30, location: "Toronto" };
person.age = 31; // ❌ TS2540: Cannot assign to 'age' because it is a read-only property.
console.log(person.age); // reading is fine- Same loop over
"name" | "age" | "location", but now the template isreadonly [P]: Person[P]— the modifier is part of the property being created. P = "name"producesreadonly name: string(Person["name"]isstring),P = "age"producesreadonly age: number,P = "location"producesreadonly location: string.- The assignment now fails with "TS2540: Cannot assign to 'age' because it is a read-only property." Reading is unaffected.
- This is exactly how the built-in
Readonly<T>is written, just withPersonreplaced by a type parameter. Like every type, it's erased at compile time: nothing stops plain JavaScript from changing the object (that's whatObject.freezeis for).
Replace Person with a type parameter and you get the generic version — the "identity" mapped type from my notes, which copies a type key by key:
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
type NewType<T> = { [P in keyof T]: T[P] };
interface Todo {
readonly id: number;
title: string;
done?: boolean;
}
type T1 = Expect<Equal<NewType<Todo>, { readonly id: number; title: string; done?: boolean }>>;
type T2 = Expect<Equal<NewType<string[]>, string[]>>; // arrays stay arrays
type T3 = Expect<Equal<NewType<[1, 2]>, [1, 2]>>; // tuples stay tuples
type T4 = Expect<Equal<NewType<string>, string>>; // primitives pass throughNewType<Todo>loops overkeyof Todo="id" | "title" | "done"and copies eachT[P]:id: number,title: string,done: boolean.- But the result also keeps
readonlyonidand?ondone. A mapped type of the form{ [P in keyof T]: … }is called homomorphic ("same shape"): because the keys come straight fromkeyof T, the checker knows eachPis a real property ofTand copies that property's modifiers onto the new one. - Homomorphic mapped types have two more special rules. Given an array or tuple type, they map the elements and produce an array or tuple again (
T2,T3) — not an object withlength,pushand friends. Given a primitive, they return it untouched (T4) — the same rule that madeReadonly<string>equalstringabove. - So "identity" is accurate: for objects, arrays, tuples and primitives,
NewType<X>isX. It's not useless, though: applied to an intersection likeA & Bit produces one flat object type, which is thePrettifyhelper you'll use in the challenges.
Where mapped types show up: every built-in object utility (Partial, Required, Readonly, Pick, Record, Omit), form-state types ({ [K in keyof Form]: string } for error messages per field), and API layers that derive "create" and "update" shapes from one model. They're also the typed alternative to index signatures when you know the keys up front — see Index signatures vs mapped types.