Mapped Types in TypeScript
Mapped Types
~14 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
Chapter 20 showed HOW to USE Partial/Readonly – this chapter shows how they actually work INTERNALLY, and how to build YOUR OWN, project-specific utility types following the same pattern.
The basic syntax of a mapped type
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Applied to Book, this produces EXACTLY the same as the built-in Partial<Book>:
// { isbn?: string; title?: string; author?: Author; ... }[K in keyof T] ITERATES over EVERY property name of T (keyof T from chapter 18) – for EACH of these keys K, a new property gets created with the same name, whose type is T[K] (the ORIGINAL type of that property). The ? additionally makes it optional – EXACTLY the definition of Partial.
Adding AND removing modifiers
// ADDING readonly (like the built-in Readonly<T>):
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// REMOVING readonly with the minus prefix:
type Writable<T> = {
-readonly [K in keyof T]: T[K];
};
// REMOVING optional (?) - EXACTLY the built-in Required<T>:
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};The - prefix before readonly/? is an EXPLICIT removal instruction – useful when you want to "loosen" an already readonly or optional source type again, which isn't directly possible with the simple utility types from chapter 20.
In practice: a change log for book updates
Let's build a custom mapped type that logs, for EVERY property of Book, whether it changed – a use case NO built-in utility type covers directly:
export type ChangeLog<T> = {
[K in keyof T]: boolean;
};
export function createChangeLog<T extends object>(
before: T,
after: T,
): ChangeLog<T> {
const log = {} as ChangeLog<T>;
for (const key in before) {
log[key] = before[key] !== after[key];
}
return log;
}const log = createChangeLog(oldBook, newBook);
// log: { isbn: boolean; title: boolean; author: boolean; ... }
if (log.title) {
console.log('The title has changed!');
}ChangeLog<Book> AUTOMATICALLY has the same property names as Book, but EACH of them is a boolean instead of the ORIGINAL type – if Book later gains a property, ChangeLog<Book> adapts AUTOMATICALLY, without you having to manually keep this mapped type in sync.
Bonus: renaming keys with as
type WithGetterPrefix<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
// Applied to { title: string; pageCount: number }:
// { getTitle: () => string; getPageCount: () => number }The as keyword INSIDE a mapped type enables "key remapping" – here combined with Capitalize (yet ANOTHER built-in utility type, for strings) and template literal types (two chapters from now), to generate a matching getter method name FROM every property name. An advanced pattern, shown here as a preview of chapter 23.
Tipp: Rule of thumb: writing mapped types YOURSELF pays off once you need a RECURRING transformation pattern across MULTIPLE types (like our ChangeLog) that no built-in utility type covers. For ONE-OFF, specific adjustments, a simple, hand-written interface is often enough.