Mapped Types
Mapped types let you create a new object type by iterating over the keys of an existing type and transforming them. Instead of copying a type manually, you describe the transformation once and TypeScript does the iteration for you.
They are the engine behind many of TypeScript's built-in utility types:
Partial, Required, Readonly, Record, Pick, and Omit are all
implemented as mapped types.
Basic Syntax
The fundamental form is:
type NewType = {
[K in SomeUnion]: ValueType;
};
K is a local type variable that iterates over every member of SomeUnion.
When SomeUnion is keyof T, you iterate over every key of type T.
// Manually iterate over a union
type Flags = {
[K in "read" | "write" | "execute"]: boolean;
};
// { read: boolean; write: boolean; execute: boolean }
// Iterate over keys of an existing type
interface User {
id: number;
name: string;
email: string;
}
type UserFlags = {
[K in keyof User]: boolean;
};
// { id: boolean; name: boolean; email: boolean }[K in keyof T] syntax iterates over every key of T. The variable K holds each key's string literal type in turn.Preserving Value Types
The most common use of mapped types is to keep each key but transform its value type.
Accessing T[K] (an indexed access type) gives you the original value type for
each key, so you can apply transformations while keeping the structure intact.
interface Product {
id: number;
name: string;
price: number;
inStock: boolean;
}
// Wrap every value in an array
type Arrayed<T> = {
[K in keyof T]: T[K][];
};
type ProductArrays = Arrayed<Product>;
// {
// id: number[];
// name: string[];
// price: number[];
// inStock: boolean[];
// }
// Make every value nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
type NullableProduct = Nullable<Product>;
// { id: number | null; name: string | null; ... }Adding Modifiers — Readonly and Optional
Mapped types can add (or remove) the readonly and ? modifiers during the
iteration. The modifier goes before the key just like in a regular object type.
// Make all properties readonly
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// Make all properties optional
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Both at once
type ReadonlyPartial<T> = {
readonly [K in keyof T]?: T[K];
};
interface Config {
host: string;
port: number;
}
type FrozenConfig = MyReadonly<Config>; // { readonly host: string; readonly port: number }
type PartialConfig = MyPartial<Config>; // { host?: string; port?: number }
type SafeConfig = ReadonlyPartial<Config>; // { readonly host?: string; readonly port?: number }Filtering Keys with never
When the value type resolves to never, TypeScript silently drops that key from the
resulting type. This gives you a way to filter keys inside a mapped type.
// Keep only keys whose value extends a given type
type PickByValue<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
};
interface Mixed {
id: number;
name: string;
active: boolean;
score: number;
}
type StringKeys = PickByValue<Mixed, string>; // { name: string }
type NumberKeys = PickByValue<Mixed, number>; // { id: number; score: number }
// The "as" clause is a key remapping — covered in depth in the next page.
// Here, returning never from "as" drops the key entirely.as clause in [K in keyof T as ...] remaps or filters keys. Returning never from it drops the key from the output type.Built-in Utility Types as Mapped Types
Every major built-in utility type is a mapped type. Seeing their implementations cements your understanding of the pattern:
// Partial<T> — makes all properties optional
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Required<T> — removes optional modifier from all properties
type Required<T> = {
[P in keyof T]-?: T[P];
};
// Readonly<T> — makes all properties readonly
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// Record<K, V> — creates an object type with keys K and value V
type Record<K extends keyof any, T> = {
[P in K]: T;
};
// Pick<T, K> — keeps only the listed keys
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
// Omit<T, K> — removes the listed keys
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;Record — Building Object Types from Key Unions
Record<K, V> is a mapped type that does not iterate over an existing type — it
iterates over a union K that you provide explicitly. This is useful for lookup
tables, dictionaries, and status maps.
type Status = "pending" | "active" | "closed";
// All statuses mapped to a label string
const statusLabels: Record<Status, string> = {
pending: "Awaiting review",
active: "Currently active",
closed: "No longer active",
};
// All statuses mapped to a config object
type StatusConfig = Record<Status, { color: string; icon: string }>;
const config: StatusConfig = {
pending: { color: "yellow", icon: "⏳" },
active: { color: "green", icon: "✅" },
closed: { color: "gray", icon: "🚫" },
};Practical Example — Getters Generator
A mapped type can rename keys using the as clause combined with template literal
types. Here is a utility that generates a getter-method interface for any data type:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User {
id: number;
name: string;
email: string;
}
type UserGetters = Getters<User>;
// {
// getId: () => number;
// getName: () => string;
// getEmail: () => string;
// }
// You can implement it with a helper function
function makeGetters<T extends object>(obj: T): Getters<T> {
const result: any = {};
for (const key in obj) {
const capitalized = key.charAt(0).toUpperCase() + key.slice(1);
result[`get${capitalized}`] = () => obj[key];
}
return result;
}
const user = { id: 1, name: "Alice", email: "alice@example.com" };
const getters = makeGetters(user);
console.log(getters.getName()); // "Alice"Recursive Mapped Types
Mapped types can be recursive, applying the transformation at every level of nesting.
This is how you build deep utilities like DeepPartial or DeepReadonly:
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
interface AppConfig {
server: {
host: string;
port: number;
tls: {
enabled: boolean;
certPath: string;
};
};
debug: boolean;
}
type PartialConfig = DeepPartial<AppConfig>;
// Now every nested level is optional:
const partial: PartialConfig = {
server: {
host: "localhost",
// port is optional
tls: {
// certPath is optional
enabled: true,
},
},
// debug is optional
};Mapped Types vs Index Signatures
Feature | Index Signature | Mapped Type |
|---|---|---|
Syntax | { [key: string]: V } | { [K in keyof T]: T[K] } |
Key constraint | string, number, or symbol | Any union or keyof T |
Per-key value types | All keys share one type | Each key can have its own type |
Key iteration source | Runtime keys | Compile-time type keys |
Built-in utilities | No | Yes (Partial, Pick, etc.) |
Homomorphic vs Non-Homomorphic Mapped Types
A mapped type is homomorphic when it iterates over keyof T directly. TypeScript
then automatically preserves (or copies) the readonly and ? modifiers from the
source type unless you explicitly change them.
A mapped type is non-homomorphic when it iterates over an independent key union
(like in Record). No modifiers are carried over.
// Homomorphic — modifiers are preserved
type Identity<T> = { [K in keyof T]: T[K] };
interface Source {
readonly id: number;
name?: string;
}
type Copied = Identity<Source>;
// { readonly id: number; name?: string }
// ✅ readonly and ? are preserved
// Non-homomorphic — no modifier preservation
type AllOptional<K extends string> = { [P in K]?: string };
type Mapped = AllOptional<"a" | "b">;
// { a?: string; b?: string } — ? was added explicitly, not copiedkeyof T, transforming value types, adding/removing modifiers, filtering with never, and all the built-in utility types that map types power.