Table of Contents
- Introduction
- What Are Mapped Types?
- Basic Syntax of Mapped Types
- Common Use Cases of Mapped Types
- Creating Read-Only and Writable Types
- Using Mapped Types with
keyofandin - Mapping with Optional and Required Properties
- Advanced Mapped Types: Nested Mapped Types
- Practical Examples
- Example 1: Creating Read-Only Properties
- Example 2: Changing Property Types Dynamically
- Example 3: Mapping Object Keys
- Best Practices for Using Mapped Types
- Conclusion
Introduction
Mapped types are one of the most powerful features of TypeScript, allowing you to dynamically create new types by transforming or modifying existing ones. With mapped types, you can iterate over the keys of a given type and transform the properties according to certain rules. They are especially useful for reducing repetitive code and creating reusable utilities for complex types.
In this article, we will dive deep into mapped types, discussing their syntax, common use cases, and advanced techniques. By the end, youโll understand how to create new types dynamically and apply them to various scenarios.
What Are Mapped Types?
Mapped types allow you to create a new type by iterating over the properties of an existing type. The transformation can be as simple as making all properties optional, read-only, or even changing the type of properties dynamically. Mapped types offer flexibility and help you reduce redundancy in type definitions.
The basic syntax of a mapped type is:
type NewType<T> = {
[K in keyof T]: SomeTransformation<T[K]>;
};
Here:
Tis the type youโre iterating over.keyof Trepresents the keys of the typeT.SomeTransformation<T[K]>defines how each property should be transformed.
Basic Syntax of Mapped Types
To understand how mapped types work, letโs start with a basic example:
type Person = {
name: string;
age: number;
};
type ReadOnlyPerson = {
readonly [K in keyof Person]: Person[K];
};
Here:
Personis a simple object type with two properties:nameandage.ReadOnlyPersonis a new type where all properties ofPersonare transformed to be read-only using thereadonlykeyword.
This creates a new type where the properties are immutable:
const person: ReadOnlyPerson = {
name: "John",
age: 30
};
person.name = "Jane"; // Error: Cannot assign to 'name' because it is a read-only property.
In this case, the mapped type dynamically applies the readonly modifier to all properties of Person.
Common Use Cases of Mapped Types
Mapped types are incredibly useful in a variety of situations, including:
- Immutability: Making all properties of a type read-only or writable.
- Optional Properties: Transforming all properties into optional ones.
- Mapping Property Types: Changing the types of certain properties dynamically.
- Creating Flexible Object Structures: Defining structures that adapt based on certain conditions.
By using mapped types, you can create flexible and reusable patterns for various common tasks, reducing code duplication.
Creating Read-Only and Writable Types
One of the most common use cases for mapped types is making all properties of an object either read-only or writable. TypeScript provides built-in utility types like Readonly and Writable, but you can easily recreate these behaviors using mapped types.
Example: Creating a Read-Only Type
type Person = {
name: string;
age: number;
};
type ReadOnlyPerson = {
readonly [K in keyof Person]: Person[K];
};
Here, ReadOnlyPerson will have all properties of Person marked as readonly. You can use this technique to apply immutability to any type.
Example: Creating a Writable Type
type ReadOnlyPerson = {
readonly name: string;
readonly age: number;
};
type WritablePerson = {
-readonly [K in keyof ReadOnlyPerson]: ReadOnlyPerson[K];
};
In this example, the WritablePerson type removes the readonly modifier from the properties of ReadOnlyPerson, making them writable again.
Using Mapped Types with keyof and in
In a mapped type, keyof is used to get the keys of a type, and in is used to iterate over those keys. This combination makes it easy to map over every property of an object and apply a transformation.
Example: Using keyof and in for Mapping
type Person = {
name: string;
age: number;
};
type MappedPerson = {
[K in keyof Person]: string;
};
Here:
- We use
keyof Personto get all the keys of thePersontype (nameandage). - We iterate over these keys using
in, and for each key, we set its type tostring.
As a result, MappedPerson will have all properties of Person with the type string:
type MappedPerson = {
name: string;
age: string;
};
This technique allows you to transform all properties of a type in a concise and reusable way.
Mapping with Optional and Required Properties
Mapped types also allow you to create types where all properties are either optional or required.
Example: Making All Properties Optional
type Person = {
name: string;
age: number;
};
type OptionalPerson = {
[K in keyof Person]?: Person[K];
};
Here, the OptionalPerson type has all properties of Person marked as optional.
Example: Making All Properties Required
type OptionalPerson = {
name?: string;
age?: number;
};
type RequiredPerson = {
[K in keyof OptionalPerson]-?: OptionalPerson[K];
};
In this example:
RequiredPersonis a mapped type that makes all properties ofOptionalPersonrequired again.
Advanced Mapped Types: Nested Mapped Types
You can use mapped types to create deeply nested types as well. This allows you to transform complex nested structures dynamically.
Example: Nested Mapped Types
type Person = {
name: string;
address: {
street: string;
city: string;
};
};
type ReadOnlyPerson = {
readonly [K in keyof Person]: Person[K] extends object ? ReadOnlyPerson[Extract<keyof Person[K], string>] : Person[K];
};
In this example:
- The mapped type recursively marks all properties of the nested
addressobject asreadonly.
This type transforms every property in the object and its nested objects, providing deep immutability.
Practical Examples
Example 1: Creating Read-Only Properties
type Person = {
name: string;
age: number;
};
type ReadOnlyPerson = {
readonly [K in keyof Person]: Person[K];
};
In this example:
ReadOnlyPersonwill have all properties ofPersonasreadonly.
Example 2: Changing Property Types Dynamically
type Person = {
name: string;
age: number;
};
type MappedPerson = {
[K in keyof Person]: string;
};
Here:
MappedPersondynamically changes all properties ofPersontostringtype.
Example 3: Mapping Object Keys
type Person = {
name: string;
age: number;
};
type MappedPerson = {
[K in keyof Person]: Person[K] extends string ? number : Person[K];
};
In this case:
- We change the type of
nametonumberif the property type isstring.
Best Practices for Using Mapped Types
- Keep It Simple: While mapped types are powerful, avoid overly complex transformations that make the code hard to understand and maintain.
- Use Built-in Utility Types When Possible: TypeScript has many built-in utility types like
Readonly,Partial, andRequired. Use these when they fit your needs, as they are well-tested and optimized. - Be Careful with Recursive Mapped Types: When dealing with deeply nested structures, use recursive mapped types carefully to avoid infinite recursion or performance issues.
Conclusion
Mapped types are a versatile feature in TypeScript that allow you to create new types by transforming existing ones. They are essential for creating reusable utilities, reducing redundancy, and improving the maintainability of your code.
By mastering mapped types, you can dynamically apply changes to object types, such as making properties optional, required, or read-only, as well as altering the types of properties. Whether you’re working with simple objects or deeply nested structures, mapped types provide the flexibility to adapt your types to your needs.

