Mapped Utility Types Explained for React
Mapped utility types use TypeScript's mapped type syntax to transform one type into another programmatically. Using the in keyof operator, you can iterate over an object's properties and create a new type that derives properties from the original, enabling you to build flexible utility types like Pick, Omit, Record, and Partial that adapt your component API to various scenarios without repeating type definitions.
What Are Mapped Types?
A mapped type transforms each property in an existing type into a new property with a potentially different name or value type. The syntax { [K in keyof T]: U } means: for each key K in type T, create a property with that key whose value is type U. This is TypeScript's way of iterating over type properties.
Mapped types are commonly used to build utility types that modify all properties of an object. For example, you might want a version of your form data type where every field is optional, or a version where every field becomes readonly.
Common Built-in Mapped Utilities
TypeScript provides several pre-built mapped utility types. Record<K, V> creates an object with keys from K and values of type V. Pick<T, K> extracts specific properties from a type. Omit<T, K> removes specific properties. Partial<T> makes all properties optional. Required<T> makes all properties required. Readonly<T> makes all properties readonly:
// Original type
interface User {
id: string;
name: string;
email: string;
age: number;
}
// Pick: select specific properties
type UserPreview = Pick<User, "id" | "name">;
// Result: { id: string; name: string }
// Omit: exclude specific properties
type UserInput = Omit<User, "id">;
// Result: { name: string; email: string; age: number }
// Partial: make all properties optional
type PartialUser = Partial<User>;
// Result: { id?: string; name?: string; email?: string; age?: number }
// Required: make all properties required
type RequiredUser = Required<Partial<User>>;
// Result: { id: string; name: string; email: string; age: number }
// Readonly: make all properties readonly
type ReadonlyUser = Readonly<User>;
// Result: { readonly id: string; readonly name: string; ... }
// Record: create an object with specific keys
type Role = "admin" | "user" | "guest";
type RolePermissions = Record<Role, string[]>;
// Result: { admin: string[]; user: string[]; guest: string[] }
These utilities enable React components to adapt types without manual duplication. For example, an edit form component might use Partial<User> to allow partial updates:
interface EditFormProps {
user: User;
onSave: (updates: Partial<User>) => void;
}
export const EditForm = ({ user, onSave }: EditFormProps) => {
const [form, setForm] = React.useState<Partial<User>>({});
const handleSave = () => {
onSave(form);
};
return (
<form>
<input
placeholder="Name"
value={form.name ?? ""}
onChange={(e) => setForm({ ...form, name: e.target.value })}
/>
<button onClick={handleSave}>Save</button>
</form>
);
};
Building Custom Mapped Types
Beyond built-in utilities, you can create custom mapped types for your specific needs. For instance, a Getters type transforms all properties into getter functions:
// Transform each property into a getter function
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface State {
count: number;
message: string;
}
type StateGetters = Getters<State>;
// Result:
// {
// getCount: () => number;
// getMessage: () => string;
// }
class Store implements StateGetters {
getCount = () => 1;
getMessage = () => "Hello";
}
Another common pattern is creating a "setter" type for form state. You can also conditionally filter properties based on their types:
// Extract only string properties
type StringPropertiesOnly<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Product {
id: number;
name: string;
sku: string;
price: number;
}
type ProductStrings = StringPropertiesOnly<Product>;
// Result: { name: string; sku: string }
// Use in component for text-only form fields
interface ProductFormProps {
onUpdate: (updates: ProductStrings) => void;
}
Mapped Type Patterns Table
| Pattern | Purpose | Example |
|---|---|---|
Pick<T, K> | Extract specific keys | Pick<User, "id" | "name"> |
Omit<T, K> | Remove specific keys | Omit<User, "password"> |
Record<K, V> | Create object with keys | Record<Role, Permission[]> |
Partial<T> | Make all optional | Partial<FormData> |
Required<T> | Make all required | Required<Config> |
Readonly<T> | Make all readonly | Readonly<State> |
{[K in keyof T]: U} | Custom transform | Any per-property transformation |
Key Takeaways
- Mapped types iterate over object properties using
in keyofsyntax, transforming one type into another. - Built-in utilities like
Pick,Omit,Partial, andRecordsolve common transformation patterns without manual duplication. - Custom mapped types enable domain-specific transformations like filtering by type, renaming keys, or changing property signatures.
- Mapped types reduce boilerplate and keep your component types DRY (don't repeat yourself).
- Combining mapped types with conditional types creates powerful, adaptive component APIs.
Frequently Asked Questions
Can I use mapped types to add or remove properties dynamically?
Yes, mapped types iterate over existing properties, but you can use as clauses to rename or filter. For example, [K in keyof T as K extends string ? K : never] filters to string-only keys. You cannot add entirely new properties with mapped types; use intersection types for that.
What's the performance impact of complex mapped types?
Deeply nested or highly recursive mapped types can slow TypeScript compilation. For most React components, simple mapped types (one or two levels deep) have negligible impact. Profile your build time if you suspect mapped types are slowing compilation.
How do I handle mapped types with readonly properties?
TypeScript respects readonly in mapped types. When you write Readonly<T>, each property becomes readonly. If you need to remove readonly modifiers, use the -readonly syntax: { -readonly [K in keyof T]: T[K] }.
Can mapped types be used in runtime code?
No. Mapped types exist only at compile time and are completely erased during JavaScript transpilation. They never affect runtime behavior or bundle size.