TypeScript Mapped Types
In TypeScript, we often need to create types that are derived from existing ones. Instead of manually defining related types, TypeScript offers a powerful feature called mapped types that allows you to create new types by transforming properties of existing ones.
Introduction to Mapped Types
Mapped types allow you to take an existing type and transform each property according to a rule you define. This is especially useful when you want to create variations of types without duplicating code.
Think of mapped types as a way to iterate over the properties of an existing type and apply transformations to each property.
Basic Syntax
The basic syntax of a mapped type looks like this:
type MappedType<T> = {
[P in keyof T]: TransformedType;
};
Let's break this down:
[P in keyof T]
: This is the mapping clause where:keyof T
creates a union of all property names in typeT
P in
iterates over each property name
TransformedType
: The resulting type for each property
Basic Examples
Making all properties optional
One common use case is to create a version of a type where all properties are optional:
// Original type
interface User {
id: number;
name: string;
email: string;
isAdmin: boolean;
}
// Mapped type to make all properties optional
type PartialUser = {
[P in keyof User]?: User[P];
};
// Usage
const completeUser: User = {
id: 1,
name: "John Doe",
email: "[email protected]",
isAdmin: false
};
const partialUser: PartialUser = {
id: 2,
name: "Jane Doe"
// email and isAdmin can be omitted
};
Making all properties readonly
Another useful transformation is to create a version where all properties are read-only:
// Mapped type to make all properties readonly
type ReadonlyUser = {
readonly [P in keyof User]: User[P];
};
const user: ReadonlyUser = {
id: 3,
name: "Alice",
email: "[email protected]",
isAdmin: true
};
// This would cause a TypeScript error
// user.name = "New Name"; // Cannot assign to 'name' because it is a read-only property
Built-in Mapped Types
TypeScript provides several built-in mapped types to save you from writing common transformations:
Partial<T>
Makes all properties optional:
// Built-in Partial
type PartialUser = Partial<User>;
// Equivalent to manually defining:
// type PartialUser = {
// [P in keyof User]?: User[P];
// };
Required<T>
Makes all properties required (removes optionality):
interface PartialConfig {
port?: number;
host?: string;
debug?: boolean;
}
// Make all properties required
type CompleteConfig = Required<PartialConfig>;
// Now all properties must be specified
const config: CompleteConfig = {
port: 8080,
host: "localhost",
debug: true
};
Readonly<T>
Makes all properties readonly:
type ReadonlyUser = Readonly<User>;
// Equivalent to:
// type ReadonlyUser = {
// readonly [P in keyof User]: User[P];
// };
Pick<T, K>
Creates a type by picking specific properties from another type:
// Create a type with just name and email
type UserCredentials = Pick<User, "name" | "email">;
const credentials: UserCredentials = {
name: "John Doe",
email: "[email protected]"
// Other User properties are not allowed here
};
Record<K, T>
Creates an object type with properties of type K
and values of type T
:
type UserRoles = Record<string, boolean>;
const roles: UserRoles = {
admin: true,
editor: true,
viewer: false
// Any string key with boolean value is allowed
};
Advanced Mapped Types
Property Remapping
You can change the property keys in a mapped type using the as
keyword:
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// Results in:
// {
// getName: () => string;
// getAge: () => number;
// }
const personGetters: PersonGetters = {
getName: () => "John Doe",
getAge: () => 30
};
console.log(personGetters.getName()); // "John Doe"
console.log(personGetters.getAge()); // 30
Filtering Properties
You can filter properties using never
with a conditional type:
// Keep only string properties
type StringProperties<T> = {
[P in keyof T as T[P] extends string ? P : never]: T[P];
};
interface Mixed {
name: string;
age: number;
address: string;
isActive: boolean;
}
type StringsOnly = StringProperties<Mixed>;
// Results in:
// {
// name: string;
// address: string;
// }
Modifying Property Types
You can transform the type of each property:
// Convert all properties to string type
type StringifyProps<T> = {
[P in keyof T]: string;
};
interface User {
id: number;
isAdmin: boolean;
registeredAt: Date;
}
type StringUser = StringifyProps<User>;
// Results in:
// {
// id: string;
// isAdmin: string;
// registeredAt: string;
// }
const stringUser: StringUser = {
id: "1",
isAdmin: "true",
registeredAt: "2023-05-15"
};
Real-World Applications
Form State Management
Mapped types are perfect for managing form state, where you might need different variations of the same data structure:
interface UserFormData {
username: string;
email: string;
password: string;
age: number;
}
// Form values (the actual data)
type FormValues = UserFormData;
// Form error messages (one error message per field)
type FormErrors = {
[K in keyof UserFormData]?: string;
};
// Form touched state (which fields have been interacted with)
type FormTouched = {
[K in keyof UserFormData]: boolean;
};
// Example usage
const formValues: FormValues = {
username: "johndoe",
email: "[email protected]",
password: "securepass",
age: 30
};
const formErrors: FormErrors = {
email: "Please enter a valid email",
password: "Password must be at least 8 characters"
};
const formTouched: FormTouched = {
username: true,
email: true,
password: true,
age: false
};
API Response Types
When working with APIs, you often need to handle different states of the data:
interface User {
id: number;
name: string;
email: string;
}
// Loading state for each field
type LoadingState<T> = {
[P in keyof T]: boolean;
};
// API response wrapper
interface ApiResponse<T> {
data: T | null;
loading: LoadingState<T>;
errors: Partial<Record<keyof T, string>>;
}
// Usage
const userResponse: ApiResponse<User> = {
data: {
id: 1,
name: "John Doe",
email: "[email protected]"
},
loading: {
id: false,
name: false,
email: false
},
errors: {
// No errors
}
};
// During loading
const loadingResponse: ApiResponse<User> = {
data: null,
loading: {
id: true,
name: true,
email: true
},
errors: {}
};
Permission Systems
Mapped types can be useful for creating permission systems:
// Define all possible actions
type ResourceActions = {
read: boolean;
create: boolean;
update: boolean;
delete: boolean;
};
// Create permissions for different resources
type Permissions = {
[Resource in 'users' | 'posts' | 'comments']: ResourceActions;
};
// Admin permissions
const adminPermissions: Permissions = {
users: { read: true, create: true, update: true, delete: true },
posts: { read: true, create: true, update: true, delete: true },
comments: { read: true, create: true, update: true, delete: true }
};
// Editor permissions
const editorPermissions: Permissions = {
users: { read: true, create: false, update: false, delete: false },
posts: { read: true, create: true, update: true, delete: false },
comments: { read: true, create: true, update: true, delete: true }
};
Combining Mapped Types with Conditional Types
Mapped types become even more powerful when combined with conditional types:
// Convert arrays to tuples and other types remain the same
type DeepArrayToTuple<T> = T extends any[]
? { [K in keyof T]: DeepArrayToTuple<T[K]> } & { length: T['length'] }
: T;
// Example usage
type StringArray = string[];
type StringTuple = DeepArrayToTuple<StringArray>;
// Nullable properties - make properties nullable based on a condition
type NullableIf<T, Condition> = {
[P in keyof T]: P extends Condition ? T[P] | null : T[P];
};
interface User {
id: number;
name: string;
email: string;
avatar: string;
}
// Make avatar nullable
type UserWithNullableAvatar = NullableIf<User, 'avatar'>;
// Results in: { id: number; name: string; email: string; avatar: string | null; }
Summary
TypeScript mapped types are a powerful feature that allows you to:
- Transform existing types into new ones using systematic rules
- Modify property modifiers (like
readonly
and?
) - Filter and rename properties
- Transform property types
- Create reusable type transformations
This makes your TypeScript code more DRY (Don't Repeat Yourself) by reducing type duplication and enabling powerful type abstractions.
Exercises
To strengthen your understanding of mapped types, try these exercises:
- Create a mapped type that converts all property types in an interface to Promise versions of those types
- Implement a
DeepReadonly<T>
type that makes all properties and nested properties readonly - Create a mapped type that converts all methods in a class to async methods
- Implement a
Mutable<T>
type that removes readonly modifiers from all properties - Create a type that extracts only method properties from an object type
Additional Resources
By mastering mapped types, you'll be able to create more flexible and maintainable TypeScript code with powerful type transformations tailored to your specific needs.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)