Mapped types and conditional types are powerful features in TypeScript that allow us to create new, complex types based on existing ones. Let’s briefly define each concept before diving into examples on how to use them together.
**Mapped Types** allow us to create new types by transforming the properties of an existing type. They use a ‘for...in‘ clause to iterate through each property in the original type.
**Conditional types** are a way to express type relationships that depend on the relationship between types themselves. They allow you to create types based on conditions, using the syntax ‘T extends U ? X : Y‘.
Now, let’s see an example of how to use mapped types with conditional types:
type MappedConditional<T> = {
[K in keyof T]: T[K] extends (infer U)[] ? U : T[K];
};
The ‘MappedConditional‘ type takes an object type ‘T‘ as its generic parameter, iterates through each property, and checks if the property’s value (‘T[K]‘) is an array. If so, it maps the array element’s type (‘U‘) as the output type, else it maps the original type ‘T[K]‘.
Consider the following example:
type ExampleType = {
anArray: number[];
stringWithValue: string;
nestedArray: string[][];
};
type TransformedExampleType = MappedConditional<ExampleType>;
‘TransformedExampleType‘ would be equivalent to the following type:
type TransformedExampleType = {
anArray: number;
stringWithValue: string;
nestedArray: string[];
};
Here’s another example to understand better how conditional types work within the mapped type:
type ExtractedReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type MappedReturnTypes<T> = {
[K in keyof T]: ExtractedReturnType<T[K]>;
};
function foo(): number {
return 42;
}
function bar(): string {
return 'hello';
}
type FunctionsType = {
fooFunction: typeof foo;
barFunction: typeof bar;
};
type ReturnTypes = MappedReturnTypes<FunctionsType>;
In this example, we first create a utility type ‘ExtractedReturnType‘. This type takes ‘T‘ as its generic parameter, checks if ‘T‘ is a function, and gets the return type of the function (‘R‘) using the ‘infer‘ keyword. If ‘T‘ is not a function, it resolves to ‘never‘.
Then, we create the mapped type ‘MappedReturnTypes‘ that goes over each property in the generic ‘T‘ and maps them to their respective return types using the ‘ExtractedReturnType‘ utility type.
In our example, ‘MappedReturnTypes<FunctionsType>‘ creates a new type ‘ReturnTypes‘ that contains the return types of the function properties:
type ReturnTypes = {
fooFunction: number;
barFunction: string;
};
In summary, using mapped types with conditional types in TypeScript enables you to create new, complex types based on the properties and structure of existing types. These powerful features help ensure type safety and code maintainability when working with a variety of data structures and functions.