Mapped types
Partial, Readonly, and Record aren't special compiler magic — they're all built out of one general mechanism you can use yourself: looping over a type's keys to build a new type, the same way a map() loops over an array's elements.
3 min read
The mechanism, stripped down to its simplest form
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
interface User {
id: number;
name: string;
}
type PartialUser = MyPartial<User>;
// { id?: number; name?: string; } — this is a hand-written version of the real Partial<T>[K in keyof T] is a mapped type: it iterates over every key in keyof T (the union of T's property-name literal types, from the generics-constraints lesson), and for each one, produces a corresponding property in the new type. T[K] (an indexed access type) looks up that specific property's real type on T. This is genuinely the same mechanism the built-in Partial<T> uses internally — the utility types from the previous lesson aren't compiler-privileged magic, they're mapped types someone already wrote, shipped with TypeScript so nobody has to write them again.
Modifiers: adding and removing readonly/? explicitly
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
type MyRequired<T> = {
[K in keyof T]-?: T[K]; // the -? REMOVES optionality instead of adding it
};
type MyMutable<T> = {
-readonly [K in keyof T]: T[K]; // the -readonly REMOVES readonly-ness
};readonly before the mapped key adds that modifier to every generated property, mirroring the built-in Readonly<T>. The - prefix on a modifier (-?, -readonly) does the opposite — it strips that modifier from every property, even if the source type already had it. This is how Required<T> is actually implemented internally: not by "not adding an optional marker," but by explicitly removing whatever optionality the source type already declared.
as: renaming keys while mapping — key remapping
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }The as clause inside a mapped type lets you compute a different key name for each generated property, instead of reusing the original key as-is — here, combined with a template literal type (`get${...}`, which builds string literal types the same way a JavaScript template string builds runtime strings) and the built-in Capitalize<T> utility, to programmatically derive getName/getAge from name/age. This is genuinely advanced territory, but it's exactly the mechanism behind auto-generated getter/setter APIs and ORMs that derive a rich, fully-typed API surface from a single base type definition.
Where this matters: deriving one type from another instead of maintaining two by hand
interface UserForm {
name: string;
email: string;
age: number;
}
// Without mapped types — a second, hand-maintained type that WILL drift eventually
interface UserFormErrors {
name?: string;
email?: string;
age?: string;
}
// With a mapped type — automatically stays in sync with UserForm forever
type FormErrors<T> = {
[K in keyof T]?: string;
};
type UserFormErrors2 = FormErrors<UserForm>;The real, practical payoff of mapped types isn't writing your own Partial-style utility from scratch (the built-in ones already cover that) — it's this pattern: deriving a related but structurally different type (here, "the same fields, but each one optionally holds an error message string instead of its real value") directly from a single source-of-truth type, so adding a field to UserForm automatically, silently, updates UserFormErrors2 too, with zero risk of the two definitions quietly drifting apart the way two independently hand-written interfaces eventually do.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What does `[K in keyof T]` actually do inside a mapped type?
2. What does the `-` prefix do in a modifier like `[K in keyof T]-?: T[K]`?
3. What is the real, practical payoff of mapped types described in this lesson?