TypeScript mapped types are one of the most powerful features for type transformation and manipulation. This guide will help you understand how to use mapped types effectively to create flexible and reusable type definitions.
Mapped types allow you to create new types based on existing ones by transforming each property according to a rule you define. Think of them as array map() operations but for types instead of values.
Table of Contents
- What are Mapped Types?
- Basic Syntax of Mapped Types
- Common Use Cases
- Advanced Transformations
- Best Practices
- Common Gotchas and Solutions
- Integration with Other TypeScript Features
- Conclusion
What are Mapped Types?
Mapped types build new types by iterating over existing ones and applying transformations. The syntax might look intimidating at first, but we’ll break it down step by step.
type ReadonlyVersion<T> = {
readonly [P in keyof T]: T[P];
};
Code language: HTML, XML (xml)
This simple example creates a new type where all properties are readonly. Let’s understand how it works.
Basic Syntax of Mapped Types
The basic syntax follows this pattern:
type NewType<T> = {
[P in keyof T]: TransformedType
};
Code language: HTML, XML (xml)
Let’s break down each part:
NewType<T>
: The name of our mapped type with a generic parameter[P in keyof T]
: Iteration over all properties in TTransformedType
: The type transformation we want to apply
Common Use Cases
Making Properties Optional
type Partial<T> = {
[P in keyof T]?: T[P];
};
interface User {
name: string;
age: number;
}
// All properties become optional
type PartialUser = Partial<User>;
Code language: PHP (php)
Making Properties Required
type Required<T> = {
[P in keyof T]-?: T[P];
};
interface Config {
debug?: boolean;
cache?: boolean;
}
// All properties become required
type RequiredConfig = Required<Config>;
Code language: PHP (php)
Advanced Transformations
Property Name Transformations
You can transform property names using template literal types:
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface Person {
name: string;
age: number;
}
// Creates: { getName: () => string, getAge: () => number }
type PersonGetters = Getters<Person>;
Code language: JavaScript (javascript)
Conditional Type Transformations
Combine mapped types with conditional types for more complex transformations:
type NonNullableProps<T> = {
[P in keyof T]: NonNullable<T[P]>;
};
interface UserData {
name: string | null;
age: number | undefined;
}
// Removes null and undefined from all properties
type RequiredUserData = NonNullableProps<UserData>;
Code language: PHP (php)
Best Practices
1. Keep Transformations Simple
Avoid complex nested transformations that make your types hard to understand:
// Good
type ReadonlyDeep<T> = {
readonly [P in keyof T]: T[P] extends object ? ReadonlyDeep<T[P]> : T[P];
};
// Avoid complex nested transformations
type ComplexTransform<T> = {
[P in keyof T]: T[P] extends object
? { [K in keyof T[P]]: T[P][K] extends Array<infer U>
? Array<ComplexTransform<U>>
: T[P][K] }
: T[P];
};
Code language: JavaScript (javascript)
2. Use Built-in Mapped Types
TypeScript provides several built-in mapped types. Use them when possible instead of creating your own:
// Use built-in Partial
type PartialConfig = Partial<Config>;
// Instead of creating your own
type CustomPartial<T> = {
[P in keyof T]?: T[P];
};
Code language: JavaScript (javascript)
3. Document Complex Transformations
Add comments explaining complex type transformations:
/**
* Creates a new type where all properties are functions
* that return the original property type
*/
type Functionalize<T> = {
[P in keyof T]: () => T[P];
};
Code language: JavaScript (javascript)
Common Gotchas and Solutions
1. Handling Union Types
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type Union = { a: string } | { b: number };
type Intersection = UnionToIntersection<Union>; // { a: string } & { b: number }
Code language: JavaScript (javascript)
2. Preserving Modifiers
type PreserveModifiers<T> = {
[P in keyof T]: T[P];
} & {};
interface Original {
readonly id: number;
optional?: string;
}
type Preserved = PreserveModifiers<Original>; // Keeps readonly and optional
Code language: PHP (php)
Integration with Other TypeScript Features
Mapped types work well with other TypeScript features like conditional types and template literal types. Here’s an example combining multiple features:
type PropertyTypes<T> = {
[P in keyof T as `${string & P}Type`]: T[P] extends object
? 'object'
: T[P] extends Function
? 'function'
: 'primitive';
};
interface Example {
name: string;
age: number;
data: object;
handler: () => void;
}
// Creates: {
// nameType: 'primitive';
// ageType: 'primitive';
// dataType: 'object';
// handlerType: 'function';
// }
type Types = PropertyTypes<Example>;
Code language: JavaScript (javascript)
Conclusion
Mapped types are a powerful feature in TypeScript that allows for flexible type transformations. They’re especially useful when working with complex type systems and when you need to apply consistent transformations across multiple types.
For more TypeScript features, check out our guide on TypeScript Type Aliases which pairs well with mapped types for creating more maintainable type definitions.
Start simple with basic transformations and gradually move to more complex patterns as you become comfortable with the syntax. Remember to prioritize readability and maintainability in your type definitions.