last modified March 5, 2025
Mapped types in TypeScript allow you to create new types by transforming the properties of an existing type. They are powerful tools for type manipulation, enabling flexible and reusable type definitions. This tutorial explores mapped types with practical examples.
Mapped types use the { [P in K]: T } syntax to iterate over a set
of keys (K) and define new property types (T) based on
an existing type. Combined with features like keyof, they enable
dynamic type transformations while maintaining type safety.
A basic mapped type transforms all properties of an existing type into a new type with a uniform modification.
type User = {
name: string;
age: number;
};
type ReadonlyUser = {
readonly [K in keyof User]: User[K];
};
const user: ReadonlyUser = { name: "Alice", age: 25 };
// user.name = "Bob"; // Error: Cannot assign to 'name' because it is read-only
console.log(user.name); // Output: Alice
In this example, ReadonlyUser is a mapped type that iterates over
the keys of User (name and age) using
keyof User and applies the readonly modifier to each
property, preserving their original types (User[K]).
The resulting type ensures user cannot be modified after
initialization—attempting user.name = "Bob" triggers a compile-time
error. The output, "Alice," confirms the property's value. This demonstrates how
mapped types can systematically transform properties, here enforcing
immutability, which is useful for protecting data integrity.
Mapped types can make all properties of an existing type optional.
type Person = {
name: string;
age: number;
city: string;
};
type OptionalPerson = {
[P in keyof Person]?: Person[P];
};
const person: OptionalPerson = { name: "Bob" };
console.log(person); // Output: { name: "Bob" }
The OptionalPerson mapped type uses ? to make all
properties of Person optional. It iterates over keyof
Person (name, age, city) and
applies the optional modifier while retaining the original types
(Person[P]). This allows person to omit
age and city without errors, unlike the required
structure of Person.
The output, { name: "Bob" }, shows this flexibility. This
transformation is ideal for scenarios like partial updates or forms where not
all fields are mandatory, showcasing mapped types' ability to adapt strict
types.
Mapped types can transform property types, such as converting all properties to a different type.
type Config = {
timeout: number;
retries: number;
};
type StringConfig = {
[K in keyof Config]: string;
};
const config: StringConfig = { timeout: "5000", retries: "3" };
console.log(config.timeout); // Output: 5000
Here, StringConfig transforms all properties of Config
from number to string. The mapped type iterates over
keyof Config (timeout, retries) and
redefines each property's type as string, ignoring the original
Config[K] types. The config object thus accepts string
values, and TypeScript enforces this—timeout: 5000 would fail.
The output, "5000," reflects the transformed type. This illustrates how mapped types can overhaul property types, useful for adapting data formats (e.g., string-based configurations) while maintaining structural consistency.
Mapped types can filter properties based on conditions using conditional types.
type Item = {
id: number;
name: string;
price: number;
};
type NumbersOnly<T> = {
[K in keyof T]: T[K] extends number ? T[K] : never;
};
const item: NumbersOnly<Item> = { id: 1, name: never, price: 100 };
// TypeScript errors if `name` is not `never`: Type 'string'
// is not assignable to type 'never'
console.log(item.price); // Output: 100
The NumbersOnly mapped type filters Item properties,
keeping only those with number types. It uses a conditional type
(T[K] extends number ? T[K] : never) within the mapping: for each
key in keyof Item, if the property type is number
(e.g., id, price), it retains it; otherwise (e.g.,
name: string), it becomes never.
The item object must assign never to
name, effectively excluding it in practice, while id
and price remain usable. The output, 100, confirms
price's inclusion. This shows how mapped types can selectively
refine types, though never properties are typically omitted in
real-world usage for cleaner objects.
Mapped types can operate over union types to generate new types for each member.
type Keys = "name" | "age";
type Flags = {
[K in Keys]: boolean;
};
const flags: Flags = { name: true, age: false };
console.log(flags.name); // Output: true
The Flags mapped type iterates over the union Keys
("name" | "age"), creating a type with each key mapped to
boolean. Unlike keyof, which works with object types,
here the keys are explicitly defined as a union, producing { name:
boolean, age: boolean }.
The flags object adheres to this structure, allowing boolean values
for each key. The output, true, reflects flags.name.
This approach is handy for creating flag-like objects or dictionaries from
predefined key sets, demonstrating mapped types' flexibility with unions beyond
object-based keys.
Mapped types can combine multiple modifiers, such as readonly and ?.
type Product = {
name: string;
price: number;
};
type OptionalReadonlyProduct = {
readonly [K in keyof Product]?: Product[K];
};
const product: OptionalReadonlyProduct = { name: "Laptop" };
// product.name = "Tablet"; // Error: Cannot assign to 'name' because it is read-only
console.log(product.name); // Output: Laptop
The OptionalReadonlyProduct mapped type applies both
readonly and ? to Product's properties.
It iterates over keyof Product (name,
price), making each property optional (?) and
immutable (readonly) while keeping the original types
(Product[K]).
The product object can omit price and cannot modify
name after initialization, as shown by the error comment. The
output, "Laptop," confirms the value. This combination is useful for defining
flexible, immutable data structures, such as optional configuration objects that
shouldn't change once set.
TypeScript's built-in utility types often use mapped types for common transformations.
type User = {
name: string;
age: number;
};
const partialUser: Partial<User> = { name: "Alice" };
const pickedUser: Pick<User, "name"> = { name: "Bob" };
console.log(partialUser.name); // Output: Alice
console.log(pickedUser.name); // Output: Bob
This example uses Partial and Pick, two utility types
built on mapped types. Partial maps over keyof
User (name, age), adding ? to make
properties optional, allowing partialUser to omit age.
Pick<User, "name"> maps only the specified key
("name"), creating a type with just that property, so
pickedUser includes only name. The outputs, "Alice"
and "Bob," reflect these transformed types. These utilities leverage mapped
types internally, simplifying common patterns like partial objects or property
selection, saving developers from writing custom mappings.
Mapped types can incorporate conditional logic to transform properties dynamically.
type Data = {
id: number;
name: string;
active: boolean;
};
type StringToNumber<T> = {
[K in keyof T]: T[K] extends string ? number : T[K];
};
const transformed: StringToNumber<Data> = { id: 1, name: 42, active: true };
console.log(transformed.name); // Output: 42
The StringToNumber mapped type conditionally transforms
Data properties: for each key in keyof Data, if the
property type extends string (name), it becomes
number; otherwise, it retains its original type (id:
number, active: boolean). The transformed
object reflects this—name must be a number (42), while
id and active stay unchanged.
The output, 42, confirms the transformation. This advanced use of mapped types with conditionals enables selective type changes, useful for data normalization or API response mapping.
Mapped types can use template literal types to generate new keys dynamically.
type Event = "click" | "hover";
type EventHandlers = {
[K in `on${Capitalize<Event>}`]: () => void;
};
const handlers: EventHandlers = {
onClick: () => console.log("Clicked"),
onHover: () => console.log("Hovered")
};
handlers.onClick(); // Output: Clicked
The EventHandlers mapped type uses a template literal to transform
the Event union ("click" | "hover") into keys like
onClick and onHover. The Capitalize
utility capitalizes each event, and the mapping assigns a function type
(() => void) to each key.
The handlers object adheres to this, defining methods for each
generated key. Calling onClick logs "Clicked," as shown. This
technique is powerful for creating dynamic APIs (e.g., event systems) from
simple unions, leveraging mapped types' ability to manipulate keys creatively.
keyof with
mapped types to ensure all properties are accounted for, maintaining type
consistency.Partial or Pick for common transformations before
writing custom mapped types.TypeScript Mapped Types Documentation
This tutorial covered TypeScript mapped types with practical examples. Use these techniques to create flexible, reusable, and type-safe code transformations.
My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.
List all TypeScript tutorials.