TypeScript Mapped Types
Mapped types are a powerful feature in TypeScript that allows you to create new types based on existing ones by transforming properties in a systematic way. They provide a way to create types that derive from other types, applying transformations to each property.
What Are Mapped Types?
Mapped types allow you to create a new type by iterating through the properties of an existing type and applying transformations to them. This is like using a map function on an array, but for types. Mapped types use the syntax [K in keyof T]
to iterate over all properties of an existing type.
Basic Syntax
The basic syntax of a mapped type looks like this:
Basic mapped type syntax
This example creates a new type that's identical to the original type. While this specific example doesn't change anything, it demonstrates the pattern that more useful mapped types follow.
Common Mapping Modifiers
Mapped types can use modifiers to change property characteristics:
Optional Properties Modifier (?)
Add or remove the optional modifier from properties:
Optional modifier
Readonly Modifier (readonly)
Add or remove the readonly modifier from properties:
Readonly modifier
Adding and Removing Modifiers
You can also remove modifiers by prefixing them with -
:
Removing modifiers
Transforming Property Keys
Mapped types can transform property keys using template literal types:
Transforming keys
Filtering Properties with Key Remapping
You can use the as
clause to filter or rename properties:
Filtering properties
Conditional Types with Mapped Types
Combining conditional types with mapped types provides even more flexibility:
Conditional mapped types
Practical Examples
Creating a Validator Type
A mapped type can create a validator object that matches the shape of your data:
Validator type
Creating Default Values
Generate a type with default values for an interface:
Default values
API Response Mapper
Create types to map API responses to your application models:
API mapper
Advanced Mapped Type Patterns
Deep Mapped Types
You can create mapped types that affect nested properties:
Deep mapped types
Template Literal Key Transformations
Mapped types can use template literals for more complex key transformations:
Template literal transformations
Best Practices for Mapped Types
- Keep it Simple: Overly complex mapped types can be difficult to understand and debug. Start with simple transformations.
- Document Your Mapped Types: Add JSDoc comments to explain what your mapped types do, especially for complex transformations.
Documenting mapped types
- Avoid Excessive Nesting: While deep nested mapped types are possible, they can lead to complexity. Consider breaking them down into smaller, composable pieces.
- Use Existing Utility Types: TypeScript provides many built-in utility types that use mapped types under the hood. Prefer these when possible.
Using built-in utility types
- Combine with Other Type Features: Mapped types are most powerful when combined with conditional types, template literals, and other TypeScript features.
Combining type features
Common Use Cases for Mapped Types
- Type Transformation: Converting types from one form to another (e.g., making properties optional, readonly, or nullable).
- API Integration: Creating types that match external API shapes or transforming between different naming conventions.
- Form Handling: Generating validation types, error message types, or touched field trackers from form data types.
- Type-Level Operations: Performing operations like picking, omitting, or filtering properties based on their types.
- Schema Definitions: Creating related types from a single source of truth, such as validators, serializers, or default values.
How Mapped Types Compare to Other TypeScript Features
Mapped Types vs. Interfaces
- Interfaces define a contract for objects to follow
- Mapped types transform existing types into new ones
Mapped types vs interfaces
Mapped Types vs. Type Aliases
- Type aliases give a name to an existing type
- Mapped types create new types by transforming existing ones
Mapped types vs type aliases
Mapped Types vs. Utility Types
- Most utility types in TypeScript are implemented using mapped types
- Use built-in utility types when possible, create custom mapped types when needed
Common Patterns with Mapped Types
Pick Pattern
Create a new type by picking specific properties:
Pick pattern
Omit Pattern
Create a new type by omitting specific properties:
Omit pattern
Record Pattern
Create a type with specified keys and value types:
Record pattern
Summary
Mapped types are a powerful feature in TypeScript that allow you to create new types by transforming existing ones. They enable:
- Property transformation (optional, readonly, different types)
- Key transformations (renaming, prefixing, filtering)
- Creating related types from a single source of truth
Combined with other TypeScript features like conditional types, template literals, and infer, mapped types provide a robust toolkit for handling complex type transformations in a clean and maintainable way.
Rather than duplicating type definitions for related concepts, mapped types let you define relationships between types. This makes your code more maintainable and enforces consistency throughout your codebase.
When working with TypeScript, consider mapped types whenever you need to derive new types that are systematic transformations of existing ones. Many common operations can be handled by TypeScript's built-in utility types, but custom mapped types enable you to craft precisely the type transformations your application needs.