Back to Blog
TypeScriptMapped TypesType SystemGenerics

TypeScript Mapped Types

Transform types with mapped types. From readonly to optional to key remapping patterns.

B
Bootspring Team
Engineering
February 13, 2021
7 min read

Mapped types transform existing types into new ones. Here's how to use them effectively.

Basic Mapped Types#

1// Iterate over keys 2type Stringify<T> = { 3 [K in keyof T]: string; 4}; 5 6interface User { 7 id: number; 8 name: string; 9 active: boolean; 10} 11 12type StringUser = Stringify<User>; 13// { id: string; name: string; active: string; } 14 15// Preserve value types 16type Clone<T> = { 17 [K in keyof T]: T[K]; 18}; 19 20// Make all properties optional 21type MyPartial<T> = { 22 [K in keyof T]?: T[K]; 23}; 24 25// Make all properties required 26type MyRequired<T> = { 27 [K in keyof T]-?: T[K]; 28}; 29 30// Make all properties readonly 31type MyReadonly<T> = { 32 readonly [K in keyof T]: T[K]; 33}; 34 35// Remove readonly 36type Mutable<T> = { 37 -readonly [K in keyof T]: T[K]; 38};

Key Filtering#

1// Filter keys by value type 2type FilterByType<T, U> = { 3 [K in keyof T as T[K] extends U ? K : never]: T[K]; 4}; 5 6interface Mixed { 7 id: number; 8 name: string; 9 count: number; 10 active: boolean; 11} 12 13type NumberProps = FilterByType<Mixed, number>; 14// { id: number; count: number; } 15 16type StringProps = FilterByType<Mixed, string>; 17// { name: string; } 18 19// Exclude keys 20type ExcludeKeys<T, K extends keyof T> = { 21 [P in keyof T as P extends K ? never : P]: T[P]; 22}; 23 24type WithoutId = ExcludeKeys<User, 'id'>; 25// { name: string; active: boolean; } 26 27// Pick keys 28type PickKeys<T, K extends keyof T> = { 29 [P in K]: T[P]; 30}; 31 32type OnlyId = PickKeys<User, 'id'>; 33// { id: number; }

Key Remapping#

1// Rename keys 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 keys 23type Prefixed<T, P extends string> = { 24 [K in keyof T as `${P}${Capitalize<string & K>}`]: T[K]; 25}; 26 27type UserPrefixed = Prefixed<User, 'user'>; 28// { userId: number; userName: string; userActive: boolean; } 29 30// Event handlers 31type EventHandlers<T> = { 32 [K in keyof T as `on${Capitalize<string & K>}Change`]: (value: T[K]) => void; 33}; 34 35type PersonHandlers = EventHandlers<Person>; 36// { onNameChange: (value: string) => void; onAgeChange: (value: number) => void; }

Conditional Mapping#

1// Different transformation based on type 2type Serialize<T> = { 3 [K in keyof T]: T[K] extends Date 4 ? string 5 : T[K] extends object 6 ? Serialize<T[K]> 7 : T[K]; 8}; 9 10interface Event { 11 id: number; 12 name: string; 13 date: Date; 14 metadata: { 15 created: Date; 16 updated: Date; 17 }; 18} 19 20type SerializedEvent = Serialize<Event>; 21// { 22// id: number; 23// name: string; 24// date: string; 25// metadata: { created: string; updated: string; } 26// } 27 28// Nullable properties 29type Nullable<T> = { 30 [K in keyof T]: T[K] | null; 31}; 32 33// Deep partial 34type DeepPartial<T> = { 35 [K in keyof T]?: T[K] extends object 36 ? DeepPartial<T[K]> 37 : T[K]; 38}; 39 40// Deep readonly 41type DeepReadonly<T> = { 42 readonly [K in keyof T]: T[K] extends object 43 ? DeepReadonly<T[K]> 44 : T[K]; 45};

Union to Object#

1// Create object type from union 2type UnionToObject<U extends string, V = boolean> = { 3 [K in U]: V; 4}; 5 6type Permissions = 'read' | 'write' | 'delete'; 7 8type PermissionFlags = UnionToObject<Permissions>; 9// { read: boolean; write: boolean; delete: boolean; } 10 11type PermissionValues = UnionToObject<Permissions, number>; 12// { read: number; write: number; delete: number; } 13 14// Record-like 15type MyRecord<K extends string | number | symbol, V> = { 16 [P in K]: V; 17}; 18 19type StringRecord = MyRecord<string, number>; 20// { [key: string]: number; }

Property Modifiers#

1// Add modifiers 2type AddOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; 3 4interface User { 5 id: number; 6 name: string; 7 email: string; 8} 9 10type UserWithOptionalEmail = AddOptional<User, 'email'>; 11// { id: number; name: string; email?: string; } 12 13// Remove modifiers from specific keys 14type RequireKeys<T, K extends keyof T> = T & Required<Pick<T, K>>; 15 16interface Config { 17 host?: string; 18 port?: number; 19 debug?: boolean; 20} 21 22type ConfigWithHost = RequireKeys<Config, 'host'>; 23// { host: string; port?: number; debug?: boolean; } 24 25// Make specific keys readonly 26type ReadonlyKeys<T, K extends keyof T> = Omit<T, K> & Readonly<Pick<T, K>>; 27 28type UserWithReadonlyId = ReadonlyKeys<User, 'id'>; 29// { readonly id: number; name: string; email: string; }

Index Signatures#

1// Mapped type with index signature 2type Dictionary<T> = { 3 [key: string]: T; 4}; 5 6type NumberDict = Dictionary<number>; 7// { [key: string]: number; } 8 9// Combine with known keys 10type ConfigMap<T> = { 11 [K in keyof T]: T[K]; 12} & { 13 [key: string]: unknown; 14}; 15 16// Indexed access in mapped types 17type ValueOf<T> = T[keyof T]; 18 19type UserValue = ValueOf<User>; 20// number | string | boolean 21 22// Get property types by key pattern 23type GettersOnly<T> = { 24 [K in keyof T as K extends `get${string}` ? K : never]: T[K]; 25};

Practical Examples#

1// API response wrapper 2type ApiResponse<T> = { 3 [K in keyof T]: { 4 data: T[K]; 5 loading: boolean; 6 error: Error | null; 7 }; 8}; 9 10interface Endpoints { 11 users: User[]; 12 posts: Post[]; 13} 14 15type ApiState = ApiResponse<Endpoints>; 16// { 17// users: { data: User[]; loading: boolean; error: Error | null; }; 18// posts: { data: Post[]; loading: boolean; error: Error | null; }; 19// } 20 21// Form state 22type FormState<T> = { 23 values: T; 24 errors: { [K in keyof T]?: string }; 25 touched: { [K in keyof T]?: boolean }; 26}; 27 28// State actions 29type Actions<T> = { 30 [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void; 31} & { 32 [K in keyof T as `reset${Capitalize<string & K>}`]: () => void; 33}; 34 35type UserActions = Actions<User>; 36// { 37// setId: (value: number) => void; 38// setName: (value: string) => void; 39// setActive: (value: boolean) => void; 40// resetId: () => void; 41// resetName: () => void; 42// resetActive: () => void; 43// }

Recursive Mapped Types#

1// Deep transform 2type DeepNullable<T> = T extends object 3 ? { [K in keyof T]: DeepNullable<T[K]> | null } 4 : T | null; 5 6// Deep required 7type DeepRequired<T> = T extends object 8 ? { [K in keyof T]-?: DeepRequired<T[K]> } 9 : T; 10 11// Flatten nested objects 12type Flatten<T, P extends string = ''> = { 13 [K in keyof T as T[K] extends object 14 ? keyof Flatten<T[K], `${P}${string & K}.`> 15 : `${P}${string & K}` 16 ]: T[K] extends object ? Flatten<T[K]>[keyof Flatten<T[K]>] : T[K]; 17}; 18 19// Path keys 20type Paths<T, P extends string = ''> = T extends object 21 ? { 22 [K in keyof T]: K extends string 23 ? P extends '' 24 ? K | Paths<T[K], K> 25 : `${P}.${K}` | Paths<T[K], `${P}.${K}`> 26 : never; 27 }[keyof T] 28 : never; 29 30interface Nested { 31 a: { 32 b: { 33 c: number; 34 }; 35 }; 36 d: string; 37} 38 39type NestedPaths = Paths<Nested>; 40// 'a' | 'a.b' | 'a.b.c' | 'd'

Combining Mapped Types#

1// Intersection of mapped types 2type Combine<T> = Getters<T> & Setters<T>; 3 4type PersonMethods = Combine<Person>; 5// { 6// getName: () => string; 7// getAge: () => number; 8// setName: (value: string) => void; 9// setAge: (value: number) => void; 10// } 11 12// Union of mapped types 13type Either<T, U> = { [K in keyof T]: T[K] } | { [K in keyof U]: U[K] }; 14 15// Merge with override 16type Merge<T, U> = { 17 [K in keyof T | keyof U]: K extends keyof U 18 ? U[K] 19 : K extends keyof T 20 ? T[K] 21 : never; 22};

Best Practices#

Design: ✓ Use meaningful type names ✓ Document complex transformations ✓ Keep transformations simple ✓ Compose small mapped types Performance: ✓ Avoid deep recursion ✓ Use constraints to limit keys ✓ Test with real types ✓ Check compilation time Patterns: ✓ Key remapping for naming conventions ✓ Conditional mapping for type variations ✓ Modifiers for optional/readonly ✓ Filtering for type subsets

Conclusion#

Mapped types transform types by iterating over keys. Use them for creating variations like readonly or optional versions, generating getter/setter types, and transforming nested structures. Key remapping enables powerful naming transformations.

Share this article

Help spread the word about Bootspring