Explore the power of mapped types in TypeScript, transforming existing types with ease. Learn through examples like Partial<T>, Required<T>, and custom mapped types.
In TypeScript, mapped types allow us to create new types by transforming existing types. This powerful feature enables us to manipulate and extend types in a flexible and dynamic way, making our code more robust and easier to maintain. In this section, we’ll explore the concept of mapped types, understand their syntax, and look at practical examples to see how they can simplify complex type manipulations.
Mapped types are a way to transform the properties of an existing type into a new type. They allow us to iterate over the keys of a type and apply transformations to each property. This can include changing property types, adding modifiers like readonly
or optional
, or even creating subsets of types.
The basic syntax for a mapped type in TypeScript looks like this:
type MappedType<T> = {
[P in keyof T]: T[P];
};
Here, T
is a generic type, P
represents each property key in T
, and T[P]
is the type of each property. This syntax allows us to iterate over each property in T
and create a new type based on it.
Let’s dive into some practical examples to see how mapped types can be used in real-world scenarios.
The Partial<T>
type is a built-in mapped type that makes all properties of T
optional. This is useful when you want to create a type where some properties may not be present.
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Example usage
interface User {
name: string;
age: number;
email: string;
}
type PartialUser = Partial<User>;
const user1: PartialUser = {
name: "Alice",
// age and email are optional
};
In this example, Partial<User>
creates a new type where all properties of User
are optional.
The Required<T>
type is another built-in mapped type that makes all properties of T
required. This is the opposite of Partial<T>
.
type Required<T> = {
[P in keyof T]-?: T[P];
};
// Example usage
interface User {
name?: string;
age?: number;
email?: string;
}
type RequiredUser = Required<User>;
const user2: RequiredUser = {
name: "Bob",
age: 30,
email: "bob@example.com",
// All properties are now required
};
Here, Required<User>
ensures that all properties of User
must be present.
Beyond the built-in types, you can create your own custom mapped types to suit specific needs.
Let’s create a custom mapped type that makes all properties of a type readonly
.
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// Example usage
interface User {
name: string;
age: number;
email: string;
}
type ReadonlyUser = Readonly<User>;
const user3: ReadonlyUser = {
name: "Charlie",
age: 25,
email: "charlie@example.com",
};
// user3.name = "Dave"; // Error: Cannot assign to 'name' because it is a read-only property.
In this example, Readonly<User>
creates a new type where all properties are readonly
, preventing them from being modified after initialization.
We can also create a mapped type that makes all properties of a type nullable.
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
// Example usage
interface User {
name: string;
age: number;
email: string;
}
type NullableUser = Nullable<User>;
const user4: NullableUser = {
name: null,
age: 30,
email: "dave@example.com",
};
Here, Nullable<User>
allows each property to be either its original type or null
.
Mapped types can be used to transform properties and add modifiers, such as readonly
, optional
, or nullable
.
Let’s create a mapped type that combines readonly
and optional
modifiers.
type ReadonlyOptional<T> = {
readonly [P in keyof T]?: T[P];
};
// Example usage
interface User {
name: string;
age: number;
email: string;
}
type ReadonlyOptionalUser = ReadonlyOptional<User>;
const user5: ReadonlyOptionalUser = {
name: "Eve",
// age and email are optional and readonly
};
In this example, ReadonlyOptional<User>
creates a type where all properties are both readonly
and optional.
Template literal types can be used in mapped types to transform keys. This allows for more advanced type manipulations.
Let’s create a mapped type that appends a suffix to each property key.
type SuffixKeys<T, S extends string> = {
[P in keyof T as `${P & string}${S}`]: T[P];
};
// Example usage
interface User {
name: string;
age: number;
email: string;
}
type UserWithSuffix = SuffixKeys<User, "Field">;
const user6: UserWithSuffix = {
nameField: "Frank",
ageField: 40,
emailField: "frank@example.com",
};
In this example, SuffixKeys<User, "Field">
creates a new type where each property key has “Field” appended to it.
Mapped types are a versatile tool in TypeScript, allowing you to create complex types with ease. Here are some ways you can experiment with mapped types:
Nullable
or ReadonlyOptional
, to suit your needs.To get a better understanding of mapped types, try modifying the examples above. For instance, create a mapped type that makes all properties readonly
and nullable, or experiment with transforming keys using different template literals.
To help visualize how mapped types work, let’s use a Mermaid.js diagram to illustrate the transformation process.
graph TD; A[Original Type] --> B[Mapped Type] B --> C[Transformed Properties] B --> D[Added Modifiers] B --> E[Transformed Keys]
Diagram Description: This diagram shows the flow from an original type to a mapped type, illustrating how properties can be transformed, modifiers added, and keys transformed.
Mapped types in TypeScript provide a powerful way to create new types by transforming existing ones. By using mapped types, you can apply transformations to properties, add modifiers, and even transform keys using template literal types. This flexibility allows you to simplify complex type manipulations and create more robust and maintainable code.
Partial<T>
and Required<T>
provide common transformations.readonly
or nullable.