Back to Blog
TypeScriptTypesMapped TypesAdvanced

TypeScript Mapped Types Guide

Master TypeScript mapped types for transforming object types. From basics to advanced patterns.

B
Bootspring Team
Engineering
May 9, 2020
7 min read

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.

Share this article

Help spread the word about Bootspring