Mapped Types
Building a brand-new type by looping over every property of an existing type and transforming each one the same way.
What is it?
You've already used utility types like Partial<T> and Record<K, V> — built-in helpers that take a type and produce a related one. Have you wondered how something like Partial<T> is actually implemented? It's not a compiler special case; it's built using a feature you can use yourself, called a mapped type.
A mapped type looks like an object type, but instead of listing properties by name, it loops over the keys of another type using a syntax similar to a for...in loop: { [K in keyof T]: ... }. keyof T gives you a union of all of T's property names, and the mapped type then produces a new property for each of those names, letting you transform the type of every property the same way — add ? to make them optional, wrap them, change their type, or even change whether they're readonly.
Explain like I'm 10
A mapped type is like running every item on a checklist through the same rubber stamp. Whatever properties the original type has, the stamp visits each one in turn and applies the same transformation — "make it optional," "make it read-only," "wrap it in a box" — without you ever having to name the properties by hand.
Examples
Reimplementing Partial and Readonly by hand
interface Task {
title: string;
done: boolean;
}
// This is essentially how the built-in Partial<T> works internally
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// And how Readonly<T> works
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
type PartialTask = MyPartial<Task>; // { title?: string; done?: boolean }
type ReadonlyTask = MyReadonly<Task>; // { readonly title: string; readonly done: boolean }[K in keyof T] loops over every key of Task ("title" and "done"), and T[K] looks up that property's original type. Adding ? or readonly in front applies that modifier to every generated property at once.
Transforming property types, not just modifiers
interface Config {
host: string;
port: number;
debug: boolean;
}
// Turn every property into a function that returns its original type
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type ConfigGetters = Getters<Config>;
// {
// getHost: () => string;
// getPort: () => number;
// getDebug: () => boolean;
// }Mapped types can also rename keys (using an as clause) and change each property's type entirely — here, every original property becomes a zero-argument function returning that property's type, with a renamed "getX" key.
How it works
When the compiler encounters { [K in keyof T]: ... }, it first resolves keyof T to the union of T's literal property-name types (for Task, that's "title" | "done"). It then iterates that union once per member, binding K to each individual key in turn, and generates one property per iteration using whatever expression appears after the colon (often T[K], an indexed access type that looks up the type of that specific property on T). The optional as clause lets each iteration rename the resulting key instead of keeping the original name. All of this happens purely at compile time — the result is a fully expanded object type with no loop or runtime cost involved.
Why does it exist?
Without mapped types, transforming every property of a type the same way (making them all optional, all readonly, all wrapped in a function) would require manually rewriting the type by hand every time the original changed, or hard-coding a small set of transformations directly into the compiler. Mapped types let library authors and everyday developers alike express "apply this transformation to every property" once, generically, for any type — which is exactly how built-in utility types like Partial, Readonly, and Record are themselves implemented.
When to use it
Reach for a mapped type when none of the built-in utility types quite do what you need — for example, turning every property into a getter function, deeply changing property names in a predictable pattern, or building a domain-specific transformation (like a "validators" object mirroring a form's fields) that you'll reuse across multiple types.
When not to use it
If a built-in utility type (Partial, Pick, Omit, Record, Readonly) already does what you need, use that directly instead of reinventing it — it's clearer to readers already familiar with the standard set. Save custom mapped types for transformations those built-ins genuinely don't cover.
Common mistakes
Forgetting that
keyof Tproduces a union of T's key names, not an array — you can't use array methods on it, only union-style operations.Writing
[K in keyof T]: Tinstead of[K in keyof T]: T[K], which repeats the same whole type for every property instead of looking up each individual property's own type.Not realizing that renaming keys requires the
asclause — writing[K in keyof T]: ...alone can transform values but never changes the key names themselves.
Practice exercises
- Easy:
Write a mapped type
Nullable<T>that turns every property of T intoT[K] | null, keeping the property required. - Medium:
Write a mapped type
Stringify<T>that turns every property of T into astring, regardless of its original type. - Hard:
Write a mapped type
EventHandlers<T>that, for an object type T describing event names mapped to payload types, produces a new type where each key is renamed toon\${Capitalize<key>}and each value is a function taking that payload and returning void.
Interview questions
What is a mapped type?
A type that generates a new object type by iterating over the keys of an existing type (via [K in keyof T]) and applying the same transformation to each resulting property.
What does `keyof T` produce?
A union type made up of the literal names of every property on T.
How are built-in utility types like `Partial<T>` implemented?
They are themselves ordinary mapped types defined in TypeScript's standard type definitions — for example, Partial<T> is { [K in keyof T]?: T[K] }.
What does the `as` clause inside a mapped type let you do that you can't do without it?
It lets each iteration rename the resulting property key using a computed expression — for example, renaming a key title to getTitle by building the new name with Capitalize and a template literal type inside the as clause. Without as, a mapped type can only transform the value of each property — the key name is always copied straight from the union being iterated.
What do the `+` and `-` modifier prefixes mean in a mapped type, and what does no prefix default to?
-readonly and -? explicitly strip that modifier from every generated property, while +readonly and +? explicitly add it. Omitting the sign (just writing readonly or ?) is shorthand for +readonly/+? — a bare mapped type can only add modifiers, never remove them, without an explicit -.
Given `type Req<T> = { [K in keyof T]-?: T[K] }`, what does `Req<{ a?: string; b: number }>` resolve to?
{ a: string; b: number } — the -? strips optionality from every property, turning the optional a into a required one; b was already required, so it's unaffected. This is exactly how the built-in Required<T> is implemented.
What makes a mapped type "homomorphic," and why does it matter?
A mapped type is homomorphic when it maps directly over [K in keyof T] for some generic T — the compiler treats it as structurally mirroring T, so it automatically copies over T's existing readonly/? modifiers (unless overridden with +/-) and preserves array/tuple shapes instead of collapsing them into a plain object. A mapped type iterating some other key union (not keyof T itself) isn't homomorphic and gets none of that automatic copying.
Given `interface Point { readonly x: number; y?: number }`, what does `type Copy<T> = { [K in keyof T]: T[K] }` produce for `Copy<Point>`, even though the mapped type never mentions `readonly` or `?`?
{ readonly x: number; y?: number } — identical to Point. Because the mapping is homomorphic (it iterates keyof T directly), TypeScript preserves each property's original modifiers automatically; they'd only change if the mapped type wrote an explicit +/-readonly or ? itself.
A mapped type written as `{ [K in keyof T]: T }` compiles but produces the wrong result — what's the mistake, and what's the fix?
It reuses the whole type T as the value for every property instead of looking up each property's own type — every generated property ends up typed as the entire object T, not its original field type. The fix is T[K] (an indexed access into T at key K), giving each property back its actual original type.
How does a mapped type differ from `Record<K, V>`?
Record<K, V> is itself a mapped type, defined as { [P in K]: V } — it applies one fixed value type V to every key in K. A custom mapped type over keyof T can instead look up each property's own original type via T[K], so different properties end up with different resulting types, which Record alone can't express.
Why does `Capitalize<string & K>` include the `string &` intersection instead of just `Capitalize<K>`?
Capitalize requires its type argument to be assignable to string, but K (bound from keyof T) is typed as string | number | symbol in general, since object keys aren't restricted to strings. Intersecting with string narrows K down to only its string-compatible part for each iteration, satisfying Capitalize's constraint.
Name the built-in string-manipulation utility types mapped types commonly combine with `as`.
Uppercase<S>, Lowercase<S>, Capitalize<S>, and Uncapitalize<S> — they transform a string literal type's casing at compile time the same way their runtime namesakes would transform an actual string value.
How would you write a mapped type `PickByType<T, V>` that keeps only the properties of T whose value type extends V?
type PickByType<T, V> = { [K in keyof T as T[K] extends V ? K : never]: T[K] } — the as clause runs a conditional per key; a key whose value doesn't extend V maps to never, and mapping a key to never inside as drops that property from the result entirely.
What does `{ [K in keyof T as never]: T[K] }` produce for any T, and why?
The empty object type {} — mapping every key's as expression to never tells the compiler to omit that property from the output, so doing it unconditionally for every key removes all of them, regardless of what T originally contained.
How is a mapped type different from an index signature like `{ [key: string]: number }`?
An index signature describes an open-ended object — any string key maps to number, and the property names aren't known ahead of time. A mapped type instead enumerates a specific, known set of keys (typically keyof T) and produces exactly one property per key it iterates — the resulting property names are fixed and visible in the type.
Can the `in` clause of a mapped type iterate over a union of string literals that isn't derived from `keyof`? Give an example.
Yes — type Flags = { [K in "draft" | "published" | "archived"]: boolean } produces { draft: boolean; published: boolean; archived: boolean }. The in clause accepts any union of key-like literal types, not only one produced by keyof T.
How would you write a mapped type that wraps every property's value in `T[K] | null`, without disturbing whatever `readonly`/`?` modifiers the original type already had?
type Nullable<T> = { [K in keyof T]: T[K] | null } — leaving off any +/-/readonly/? means the modifiers are simply inherited from T, since iterating keyof T directly makes the mapping homomorphic; only the value type expression after the colon needs to change.
Why does writing `{ [K in SomeObjectType]: ... }` fail, if `SomeObjectType` is an interface rather than a union of key names?
The in clause needs a union of key-like literal types (string, number, or symbol literals) to iterate over — an object type itself isn't one. You have to convert it first with keyof SomeObjectType, which produces the union of its property names, before the mapped type can loop over it.
You wrote a mapped type to rename every key with a `get` prefix but forgot the `as` clause, writing `{ [K in keyof T]: () => T[K] }` instead. What's wrong with the output, and would TypeScript catch it?
It compiles fine and does wrap every value in a function — but the keys are never renamed; you get { title: () => string }, not { getTitle: () => string }. TypeScript won't flag this as an error, since transforming values without renaming keys is perfectly valid; the missing as clause is a silent logic gap, not a type error.
Is there really a difference between `Partial<T>` and a hand-written `{ [K in keyof T]?: T[K] }`?
No — they're not just similar, they're identical: Partial<T> is defined in TypeScript's own standard library exactly as { [K in keyof T]?: T[K] }, so writing that mapped type yourself produces the same type the compiler generates from Partial<T>.
Does adding `readonly` inside a mapped type do anything to the object at runtime?
No. readonly, ?, and the +/- modifiers are purely compile-time constraints checked by the type system — they affect what the compiler lets you write, but generate no runtime code and don't freeze, seal, or otherwise change the actual JavaScript object produced when the code runs.
You need a type where every property becomes a validator function `(value: OriginalType) => boolean`, but the property names stay exactly the same. Do you need the `as` clause?
No — as is only needed to change the keys. Here only the value type changes, so { [K in keyof T]: (value: T[K]) => boolean } is enough; adding an as clause without actually renaming anything would be pointless.
Can a homomorphic mapped type preserve an array or tuple type instead of turning it into a plain object?
Yes — because a homomorphic mapped type (one written as [K in keyof T] over generic T) structurally mirrors whatever T actually is, passing an array or tuple type as T produces a new array/tuple of the same shape with each element type transformed, rather than collapsing into an object type keyed by numeric-looking indices.
How does combining a mapped type with a conditional in the `as` clause differ from just using `Pick<T, K>`?
Pick<T, K> requires you to already know and name the specific keys K you want. A mapped type with a conditional as clause ([K in keyof T as T[K] extends Cond ? K : never]) instead selects keys dynamically, based on a structural test against each property's value type, without enumerating them by name.