Mapped Types: Transforming Every Property at Once
Learn to build new object types by iterating over an existing type's keys with a mapped type, control optional and readonly modifiers, remap keys, and generate keys with template literals.
Mapped types let you derive a new object type from an existing one by transforming every property in a single, reusable definition, instead of retyping each property by hand. They are the mechanism behind Partial, Readonly, Record, and Pick, and combined with key remapping and template literal types they can reshape both the values and the names of an object's properties.
The mapped type syntax: [K in keyof T]
A mapped type iterates over the keys of an existing type using the syntax [K in keyof T], producing a new type with one property per key. This is conceptually similar to a for...in loop, but it runs entirely at the type level during compilation, producing a brand-new type rather than any runtime value. type Optional<T> = { [K in keyof T]?: T[K]; }; is a complete, generic mapped type: for every key K in T, it creates a property K whose value type is T[K] (an indexed access, reading the original property's type), and adds a ? to make it optional.
This is exactly how TypeScript's own built-in utility types like Partial<T> and Readonly<T> are implemented internally — they're ordinary mapped types shipped with the compiler, not special-cased language features, which is why you can build your own equivalents or variants from scratch.
You can iterate over any union of keys, not just keyof T — [K in "a" | "b" | "c"]: number is a valid mapped type producing an object with exactly those three keys, all typed as number, which is a common way to build a lookup object type without an interface.
Optional and readonly modifiers, and removing them
Inside a mapped type you can add a modifier immediately before the mapped property: ? to make each property optional, or readonly to make each property readonly. type ReadonlyVersion<T> = { readonly [K in keyof T]: T[K]; }; produces a type identical to T but where every property is locked against reassignment.
Prefixing a modifier with a minus sign, as in -readonly or -?, removes that modifier instead of adding it, which is how you write a type that strips optionality or mutability from an existing type. type Concrete<T> = { [K in keyof T]-?: T[K]; }; is the built-in Required<T> utility: it forces every optional property to become required, and is genuinely useful after a validation step has guaranteed every field is actually present.
You can combine both a readonly and an optional modifier change in a single mapped type, and a plain + before a modifier (like +readonly) is allowed but redundant with just writing the modifier directly, since adding is the default behavior when no sign is given.
Key remapping with as
TypeScript's key remapping feature lets a mapped type change not just each property's value, but the property's name itself, using an as clause after the mapped key: [K in keyof T as NewKeyExpression]: T[K]. The NewKeyExpression is itself a type, usually built from K using a template literal type, and can reference K to derive a new name from the original one.
A very common use is generating a set of getter-style method names from a data type's fields: type Getters<T> = { [K in keyof T as get${Capitalize<string & K>}]: () => T[K] }; turns a property named name into a method named getName that returns the original property's type — a pattern used heavily to auto-generate accessor APIs from plain data shapes.
Mapping a key to the never type inside the as clause causes that property to be dropped from the resulting type entirely, since never can never actually occur as a key. This is the standard technique for filtering out properties conditionally within a mapped type, for example removing every key whose value type is a function.
Template literal keys
Because the as clause accepts any type expression, and template literal types can be built dynamically from a type parameter, mapped types and template literal types combine to generate whole families of related keys from a single source type. Given an event map with keys like click and hover, a mapped type can generate a parallel type with keys onClick and onHover in one definition, without listing each event name twice.
This pattern is genuinely load-bearing in typed event systems and form libraries: rather than manually writing both a data shape and a matching handlers shape with prefixed names, you derive the handlers shape from the data shape with a single mapped type, so adding a new field to the data automatically produces a correctly named, correctly typed handler slot with zero additional code.
Mapped type syntax, modifiers, and key remapping
keyof T produces the union of T's property names, which [K in ...] then iterates over. T[K] (an indexed access type) looks up the original type of each property so the mapped type stays accurate. The as clause in a mapped type can rename or drop (via never) each generated key.
type Optional<T> = {
[K in keyof T]?: T[K];
};
type Concrete<T> = {
[K in keyof T]-?: T[K];
};
type ReadonlyVersion<T> = {
readonly [K in keyof T]: T[K];
};
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
// Key remapping with `as`
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};Code Examples
PartialUser has every property of User, but each one is now optional, useful for representing a partial update payload sent to an API. Expected result: PartialUser = { id?: number; name?: string } type Optional<T> = { [K in keyof T]?: T[K]; };
Key remapping with as builds a brand-new key name for every property using a template literal type, turning title into getTitle, price into getPrice, and so on, while T[K] preserves each original property's value type as the getter's return type.
[K in keyof T as get${Capitalize<string & K>}]: () => T[K];
Common mistake: Omitting the string & intersection before Capitalize (writing Capitalize<K> directly) fails to compile in some contexts because K's constraint is string | number | symbol, not string alone; intersecting with string narrows it to only the string-like keys.
interface User {
id: number;
name: string;
}
type Optional<T> = {
[K in keyof T]?: T[K];
};
type PartialUser = Optional<User>;
// equivalent to: { id?: number; name?: string }interface Product {
title: string;
price: number;
tags: string[];
}
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type ProductGetters = Getters<Product>;
// {
// getTitle: () => string;
// getPrice: () => number;
// getTags: () => string[];
// }How a mapped type transforms a shape
The top path adds a modifier to each key while keeping its name; the bottom path renames each key with a template literal while keeping the value transformed into a function.
type User = { id: number; name: string }
|
[K in keyof T]?: T[K]
|
type PartialUser = { id?: number; name?: string }
type User = { id: number; name: string }
|
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
|
type Getters = { getId: () => number; getName: () => string }Important Rules
Mapped types only work over an existing type's keys — you can't add new keys that weren't on the source type without combining with an intersection or the as remapping clause.
Use -readonly or -? to strip a modifier rather than add one; the minus-sign prefix is easy to miss when scanning code.
TypeScript's built-in Partial, Required, Readonly, and Pick are themselves just mapped types — reading their source is a good way to learn the pattern.
Mapping a key to never inside an as clause removes that property from the result entirely, which is the standard way to filter keys conditionally.
Where this shows up
Form libraries and update APIs constantly need a version of a data type where every field is optional, for a PATCH request or an in-progress form draft, and mapped types like Partial<T> generate that automatically instead of maintaining a second, hand-written interface that can drift out of sync. State management libraries use key-remapping mapped types to auto-derive action creator names (like setUsername from a username field) directly from a state shape, so the action API can never fall out of sync with the state it updates.
type Optional<T> = { [K in T]?: T[K] } type Optional<T> = { [K in keyof T]?: T[K] }
Forgetting keyof tries to iterate over T itself (a type, not a union of keys) rather than its property names, which is a type error since T is not necessarily a union of valid key types.
type Renamed<T> = { [K in keyof T]: as new_${K}: T[K] } type Renamed<T> = { [K in keyof T as new_${string & K}]: T[K] }
The as clause belongs immediately after the mapped key expression [K in keyof T], before the colon, not after it — and K typically needs intersecting with string before use inside a template literal type.
[multiple-choice] What does [K in keyof T] do inside a mapped type? A. Deletes all keys from T B. Iterates over every property key of T to build a new type C. Converts T to an array D. Checks if T has a key named K
B. Iterates over every property key of T to build a new type — keyof T produces a union of T's keys, and [K in ...] maps over each one to construct a new property in the resulting type.
[predict-output] What does { [K in keyof T]-readonly: T[K] } do to a type where every property was readonly? A. Makes every property optional B. Removes the readonly modifier from every property C. Deletes every property D. Nothing, it's invalid syntax
B. Removes the readonly modifier from every property — The -readonly modifier explicitly strips the readonly modifier from each mapped property.
[multiple-choice] In { [K in keyof T as get${Capitalize<string & K>}]: () => T[K] }, what does mapping a key to never inside the as clause do (for a different key)? A. Causes a compile error B. Removes that property from the resulting type entirely C. Keeps the property but types its value as never D. Renames the key to the literal string "never"
B. Removes that property from the resulting type entirely — Since never can never occur as an actual key, mapping a key to never inside an as clause is the standard technique for filtering that property out of the resulting mapped type.
Write a mapped type Nullable<T> that makes every property of T allow null in addition to its original type, and a second mapped type Stringify<T> that uses key remapping to turn every key K into str_${K} while keeping the original value type.
type Nullable<T> = { [K in keyof T]: T[K] | null; }; type Stringify<T> = { [K in keyof T as str_${string & K}]: T[K]; };
Nullable needs a union inside the mapped value: T[K] | null. Stringify needs an as clause with a template literal type built from K.