Difficulty: Advanced
What are mapped types? Show how to create custom utility types using mapped types.
Mapped types create new types by transforming each property of an existing type. They iterate over the keys of a type using the 'in keyof' syntax and produce a new type with transformed properties.
Syntax: { [K in keyof T]: NewType }
You can modify properties with: - ? / -? : add or remove optionality - readonly / -readonly : add or remove readonly - 'as' clause: rename or filter keys
Built-in utility types like Partial, Required, Readonly, and Pick are all implemented using mapped types. Understanding mapped types lets you create custom utilities tailored to your codebase.
// How Partial<T> works internally
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// How Required<T> works (-? removes optionality)
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
// How Readonly<T> works
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// How Pick<T, K> works
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
// Usage
interface User {
id: number;
name: string;
email?: string;
age?: number;
}
type PartialUser = MyPartial<User>;
// { id?: number; name?: string; email?: string; age?: number; }
type RequiredUser = MyRequired<User>;
// { id: number; name: string; email: string; age: number; }
type UserPreview = MyPick<User, 'id' | 'name'>;
// { id: number; name: string; }
Mapped types iterate over keys with [K in keyof T]. The ? and readonly modifiers can be added (+) or removed (-) to transform properties.
// Make all properties nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
// Make all properties return promises
type Async<T> = {
[K in keyof T]: Promise<T[K]>;
};
// Create getter methods for all properties
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }
// Event handler map
type EventHandlers<T> = {
[K in keyof T as `on${Capitalize<string & K>}Change`]: (value: T[K]) => void;
};
type FormHandlers = EventHandlers<{ name: string; email: string; age: number }>;
// {
// onNameChange: (value: string) => void;
// onEmailChange: (value: string) => void;
// onAgeChange: (value: number) => void;
// }
Key remapping with 'as' lets you rename keys using template literal types. Capitalize, Uppercase, Lowercase, and Uncapitalize are built-in string manipulation types.
// Filter keys by value type
type StringKeys<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}
type UserStringFields = StringKeys<User>;
// { name: string; email: string; }
// Make only specific keys optional
type OptionalExcept<T, K extends keyof T> = {
[P in K]: T[P];
} & {
[P in Exclude<keyof T, K>]?: T[P];
};
type CreateUser = OptionalExcept<User, 'name' | 'email'>;
// { name: string; email: string; id?: number; isActive?: boolean; }
// Mutable - remove readonly from all properties
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
type MutableUser = Mutable<Readonly<User>>;
// All properties are writable again
Using 'never' in key remapping filters out unwanted keys. Conditional types in the 'as' clause enable powerful type-level filtering.
Mapped Types, keyof, in operator, Modifiers, Key Remapping