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
keyof
andin
- 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:
T
is the type you’re iterating over.keyof T
represents 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:
Person
is a simple object type with two properties:name
andage
.ReadOnlyPerson
is a new type where all properties ofPerson
are transformed to be read-only using thereadonly
keyword.
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 Person
to get all the keys of thePerson
type (name
andage
). - 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:
RequiredPerson
is a mapped type that makes all properties ofOptionalPerson
required 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
address
object 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:
ReadOnlyPerson
will have all properties ofPerson
asreadonly
.
Example 2: Changing Property Types Dynamically
type Person = {
name: string;
age: number;
};
type MappedPerson = {
[K in keyof Person]: string;
};
Here:
MappedPerson
dynamically changes all properties ofPerson
tostring
type.
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
name
tonumber
if 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.