Mapped types transform existing types into new ones. Here's how to use them effectively.
Basic Mapped Types#
1// Basic syntax
2type MappedType<T> = {
3 [K in keyof T]: T[K];
4};
5
6// Make all properties optional
7type MyPartial<T> = {
8 [K in keyof T]?: T[K];
9};
10
11// Make all properties required
12type MyRequired<T> = {
13 [K in keyof T]-?: T[K];
14};
15
16// Make all properties readonly
17type MyReadonly<T> = {
18 readonly [K in keyof T]: T[K];
19};
20
21// Remove readonly
22type Mutable<T> = {
23 -readonly [K in keyof T]: T[K];
24};
25
26// Example usage
27interface User {
28 name: string;
29 age: number;
30 email?: string;
31}
32
33type PartialUser = MyPartial<User>;
34// { name?: string; age?: number; email?: string; }
35
36type RequiredUser = MyRequired<User>;
37// { name: string; age: number; email: string; }
38
39type ReadonlyUser = MyReadonly<User>;
40// { readonly name: string; readonly age: number; readonly email?: string; }Key Transformations#
1// Transform keys to different type
2type Stringify<T> = {
3 [K in keyof T]: string;
4};
5
6interface Config {
7 port: number;
8 debug: boolean;
9 host: string;
10}
11
12type StringConfig = Stringify<Config>;
13// { port: string; debug: string; host: string; }
14
15// Nullable properties
16type Nullable<T> = {
17 [K in keyof T]: T[K] | null;
18};
19
20type NullableUser = Nullable<User>;
21// { name: string | null; age: number | null; email?: string | null; }
22
23// Array of each property
24type Arrayed<T> = {
25 [K in keyof T]: T[K][];
26};
27
28type ArrayedUser = Arrayed<User>;
29// { name: string[]; age: number[]; email?: string[]; }Pick and Omit#
1// Pick specific properties
2type MyPick<T, K extends keyof T> = {
3 [P in K]: T[P];
4};
5
6interface Todo {
7 title: string;
8 description: string;
9 completed: boolean;
10 createdAt: Date;
11}
12
13type TodoPreview = MyPick<Todo, 'title' | 'completed'>;
14// { title: string; completed: boolean; }
15
16// Omit properties
17type MyOmit<T, K extends keyof T> = {
18 [P in Exclude<keyof T, K>]: T[P];
19};
20
21type TodoWithoutDates = MyOmit<Todo, 'createdAt'>;
22// { title: string; description: string; completed: boolean; }
23
24// Pick by value type
25type PickByType<T, U> = {
26 [K in keyof T as T[K] extends U ? K : never]: T[K];
27};
28
29type StringProps = PickByType<Todo, string>;
30// { title: string; description: string; }
31
32type BooleanProps = PickByType<Todo, boolean>;
33// { completed: boolean; }Key Remapping (TypeScript 4.1+)#
1// Rename keys with template literals
2type Getters<T> = {
3 [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
4};
5
6interface Person {
7 name: string;
8 age: number;
9}
10
11type PersonGetters = Getters<Person>;
12// { getName: () => string; getAge: () => number; }
13
14// Setters
15type Setters<T> = {
16 [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
17};
18
19type PersonSetters = Setters<Person>;
20// { setName: (value: string) => void; setAge: (value: number) => void; }
21
22// Prefix all keys
23type Prefixed<T, P extends string> = {
24 [K in keyof T as `${P}${string & K}`]: T[K];
25};
26
27type DataUser = Prefixed<User, 'data_'>;
28// { data_name: string; data_age: number; data_email?: string; }
29
30// Filter keys by condition
31type FilterKeys<T, Condition> = {
32 [K in keyof T as T[K] extends Condition ? K : never]: T[K];
33};
34
35type OnlyStrings = FilterKeys<Todo, string>;
36// { title: string; description: string; }Conditional Mapped Types#
1// Different transformation based on type
2type Processed<T> = {
3 [K in keyof T]: T[K] extends string
4 ? string[]
5 : T[K] extends number
6 ? number
7 : T[K];
8};
9
10interface Data {
11 name: string;
12 count: number;
13 active: boolean;
14}
15
16type ProcessedData = Processed<Data>;
17// { name: string[]; count: number; active: boolean; }
18
19// Wrap functions
20type AsyncMethods<T> = {
21 [K in keyof T]: T[K] extends (...args: infer A) => infer R
22 ? (...args: A) => Promise<R>
23 : T[K];
24};
25
26interface Sync {
27 getData(): string;
28 setData(value: string): void;
29 name: string;
30}
31
32type Async = AsyncMethods<Sync>;
33// { getData: () => Promise<string>; setData: (value: string) => Promise<void>; name: string; }Recursive Mapped Types#
1// Deep partial
2type DeepPartial<T> = {
3 [K in keyof T]?: T[K] extends object
4 ? DeepPartial<T[K]>
5 : T[K];
6};
7
8interface Nested {
9 user: {
10 profile: {
11 name: string;
12 age: number;
13 };
14 settings: {
15 theme: string;
16 };
17 };
18}
19
20type DeepPartialNested = DeepPartial<Nested>;
21// All properties at any depth become optional
22
23// Deep readonly
24type DeepReadonly<T> = {
25 readonly [K in keyof T]: T[K] extends object
26 ? DeepReadonly<T[K]>
27 : T[K];
28};
29
30// Deep required
31type DeepRequired<T> = {
32 [K in keyof T]-?: T[K] extends object
33 ? DeepRequired<T[K]>
34 : T[K];
35};
36
37// Deep nullable
38type DeepNullable<T> = {
39 [K in keyof T]: T[K] extends object
40 ? DeepNullable<T[K]>
41 : T[K] | null;
42};Record Type#
1// Built-in Record
2type MyRecord<K extends keyof any, V> = {
3 [P in K]: V;
4};
5
6// String keys with specific value type
7type StringDict = Record<string, number>;
8const scores: StringDict = { alice: 100, bob: 95 };
9
10// Union keys
11type Status = 'pending' | 'active' | 'completed';
12type StatusColors = Record<Status, string>;
13
14const colors: StatusColors = {
15 pending: 'yellow',
16 active: 'green',
17 completed: 'blue',
18};
19
20// Object with specific keys from enum
21enum Permission {
22 Read = 'read',
23 Write = 'write',
24 Delete = 'delete',
25}
26
27type PermissionFlags = Record<Permission, boolean>;
28
29const permissions: PermissionFlags = {
30 [Permission.Read]: true,
31 [Permission.Write]: true,
32 [Permission.Delete]: false,
33};Practical Patterns#
1// Form state type
2type FormState<T> = {
3 values: T;
4 errors: Partial<Record<keyof T, string>>;
5 touched: Partial<Record<keyof T, boolean>>;
6};
7
8interface LoginForm {
9 email: string;
10 password: string;
11}
12
13type LoginFormState = FormState<LoginForm>;
14// {
15// values: { email: string; password: string; };
16// errors: { email?: string; password?: string; };
17// touched: { email?: boolean; password?: boolean; };
18// }
19
20// API response wrapper
21type ApiResponse<T> = {
22 [K in keyof T]: {
23 data: T[K];
24 loading: boolean;
25 error: Error | null;
26 };
27};
28
29interface Endpoints {
30 users: User[];
31 posts: Post[];
32}
33
34type ApiState = ApiResponse<Endpoints>;
35// {
36// users: { data: User[]; loading: boolean; error: Error | null; };
37// posts: { data: Post[]; loading: boolean; error: Error | null; };
38// }
39
40// Event handlers
41type EventHandlers<T> = {
42 [K in keyof T as `on${Capitalize<string & K>}Change`]: (
43 value: T[K]
44 ) => void;
45};
46
47type UserHandlers = EventHandlers<User>;
48// {
49// onNameChange: (value: string) => void;
50// onAgeChange: (value: number) => void;
51// onEmailChange: (value: string | undefined) => void;
52// }Combining Mapped Types#
1// Intersection of mapped types
2type ReadonlyPartial<T> = Readonly<Partial<T>>;
3
4type Config = ReadonlyPartial<{
5 apiUrl: string;
6 timeout: number;
7 debug: boolean;
8}>;
9// { readonly apiUrl?: string; readonly timeout?: number; readonly debug?: boolean; }
10
11// Exclude then transform
12type PublicMethods<T> = Pick<
13 T,
14 {
15 [K in keyof T]: K extends `_${string}` ? never : K;
16 }[keyof T]
17>;
18
19interface Service {
20 getData(): string;
21 _internalMethod(): void;
22 process(): void;
23 _helper(): void;
24}
25
26type PublicService = PublicMethods<Service>;
27// { getData: () => string; process: () => void; }
28
29// Merge types with override
30type Merge<T, U> = Omit<T, keyof U> & U;
31
32interface Base {
33 id: number;
34 name: string;
35 value: string;
36}
37
38interface Override {
39 value: number;
40 extra: boolean;
41}
42
43type Merged = Merge<Base, Override>;
44// { id: number; name: string; value: number; extra: boolean; }Best Practices#
Usage:
✓ Use for type transformations
✓ Leverage key remapping
✓ Combine with conditional types
✓ Create reusable utility types
Patterns:
✓ DeepPartial for nested updates
✓ Record for dictionaries
✓ Key remapping for naming
✓ Conditional for type-based changes
Readability:
✓ Name types descriptively
✓ Break complex types into parts
✓ Add comments for non-obvious logic
✓ Use built-in utilities when possible
Avoid:
✗ Over-nesting mapped types
✗ Infinite recursion
✗ Overly complex transformations
✗ Reinventing built-in types
Conclusion#
Mapped types are powerful tools for transforming types in TypeScript. Use them to create partial, readonly, or otherwise modified versions of existing types. Key remapping enables renaming properties, while conditional mapped types allow type-specific transformations. Combine them with other TypeScript features for sophisticated type utilities.