Mapped types in TypeScript are an advanced and powerful feature that allows developers to transform existing types into new ones. By using mapped types, you can create new types by iterating over the properties of an existing type and applying transformations to each property. This is particularly useful for creating utility types that can be reused across your codebase, ensuring consistency and reducing redundancy.
At its core, a mapped type uses a construct that looks like a for-in loop, iterating over the keys of a given type. The basic syntax for a mapped type is as follows:
type MappedType<T> = {
[P in keyof T]: T[P];
};
In this syntax, T
is the original type, P
represents each property key in T
, and T[P]
represents the type of the property. This construct allows you to create a new type that mirrors the structure of the original type.
One of the most common use cases for mapped types is creating read-only versions of existing types. TypeScript provides a built-in utility type called Readonly<T>
that demonstrates this concept:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
By using the readonly
modifier, you can transform each property of the original type T
into a read-only property, preventing any modifications to the properties of the new type.
Another common use case is creating partial versions of types, where all properties are optional. TypeScript provides the Partial<T>
utility type for this purpose:
type Partial<T> = {
[P in keyof T]?: T[P];
};
In this example, the ?
modifier is applied to each property, making them optional in the new type. This is particularly useful when dealing with functions that might only need a subset of the properties defined in a type.
Mapped types can also be used to create types that transform property values. For instance, you might want to create a type where all properties are converted to a specific type, such as string
or number
. Here's an example of how you could create a type that converts all properties to string
:
type Stringify<T> = {
[P in keyof T]: string;
};
This type takes any type T
and creates a new type where all properties are of type string
, regardless of their original types.
Mapped types can be combined with conditional types to create more complex transformations. For example, you might want to create a type that makes properties optional only if they are of a certain type. Here's how you could create a type that makes properties optional if they are of type string
:
type OptionalIfString<T> = {
[P in keyof T]: T[P] extends string ? T[P] | undefined : T[P];
};
In this example, a conditional type is used to check if each property type extends string
. If it does, the property is made optional by adding | undefined
to the type.
TypeScript's utility types, such as Readonly<T>
, Partial<T>
, Required<T>
, Pick<T, K>
, and Record<K, T>
, are all implemented using mapped types. These utility types provide a foundation for creating robust and reusable type transformations.
Here's a brief overview of some of these utility types:
- Readonly<T>: Creates a type with all properties of
T
set toreadonly
. - Partial<T>: Creates a type with all properties of
T
set to optional. - Required<T>: Creates a type with all properties of
T
set to required. - Pick<T, K>: Constructs a type by picking a set of properties
K
fromT
. - Record<K, T>: Constructs a type with a set of properties
K
of typeT
.
Mapped types can also be used to enforce constraints on object properties. For instance, you might want to ensure that all properties of a type are functions. Here's how you could create a type that enforces this constraint:
type FunctionsOnly<T> = {
[P in keyof T]: T[P] extends Function ? T[P] : never;
};
In this example, a conditional type checks if each property is a function. If it is, the property is included in the new type; otherwise, it is set to never
, effectively removing it.
Another practical application of mapped types is creating types that transform the keys of an object. For example, you might want to create a type where all keys are converted to uppercase strings. Here's how you could achieve this:
type UppercaseKeys<T> = {
[P in keyof T as Uppercase<string & P>]: T[P];
};
This example uses the as
keyword to rename each key to its uppercase equivalent. The Uppercase<string & P>
construct ensures that each key is converted to an uppercase string.
Mapped types are not limited to transforming properties of a single type. They can also be used to merge properties from multiple types. For instance, you might want to create a type that combines properties from two different types:
type Merge<T, U> = {
[P in keyof T | keyof U]: P extends keyof U ? U[P] : P extends keyof T ? T[P] : never;
};
This type iterates over the union of keys from both T
and U
. If a key exists in U
, its type is used; otherwise, the type from T
is used. This allows you to create a new type that merges properties from both types, with U
taking precedence in case of conflicts.
In conclusion, mapped types in TypeScript offer a powerful way to create flexible and reusable type transformations. They enable developers to write more expressive and concise code by leveraging the ability to iterate over and transform properties of existing types. Whether you're creating utility types, enforcing constraints, or transforming property keys, mapped types provide a robust toolset for managing complex type transformations in TypeScript.