Back to Blog
TypeScriptRecordTypesDictionaries

TypeScript Record Type Patterns

Master TypeScript Record type for creating type-safe dictionaries and mappings.

B
Bootspring Team
Engineering
October 19, 2018
6 min read

The Record utility type creates object types with specific key and value types. Here's how to use it effectively.

Basic Record#

1// Record<Keys, Type> 2// Creates object type with keys of type Keys and values of type Type 3 4// String keys with specific value type 5type StringToNumber = Record<string, number>; 6 7const scores: StringToNumber = { 8 alice: 95, 9 bob: 87, 10 charlie: 92, 11}; 12 13// Union of string literals as keys 14type Status = 'pending' | 'active' | 'completed'; 15type StatusCounts = Record<Status, number>; 16 17const counts: StatusCounts = { 18 pending: 5, 19 active: 10, 20 completed: 25, 21}; 22// All keys required!

Enum Keys#

1enum Color { 2 Red = 'red', 3 Green = 'green', 4 Blue = 'blue', 5} 6 7// Record with enum keys 8type ColorHex = Record<Color, string>; 9 10const colorMap: ColorHex = { 11 [Color.Red]: '#ff0000', 12 [Color.Green]: '#00ff00', 13 [Color.Blue]: '#0000ff', 14}; 15 16// All enum values must be present 17// Missing one would be an error 18 19// Access 20const redHex = colorMap[Color.Red]; // '#ff0000'

Complex Value Types#

1interface User { 2 id: number; 3 name: string; 4 email: string; 5} 6 7// Record with complex values 8type UserMap = Record<string, User>; 9 10const users: UserMap = { 11 'user-1': { id: 1, name: 'John', email: 'john@example.com' }, 12 'user-2': { id: 2, name: 'Jane', email: 'jane@example.com' }, 13}; 14 15// Nested records 16type NestedConfig = Record<string, Record<string, string>>; 17 18const config: NestedConfig = { 19 database: { 20 host: 'localhost', 21 port: '5432', 22 }, 23 cache: { 24 host: 'redis', 25 port: '6379', 26 }, 27};

Mapped Type Comparison#

1// Record is a built-in mapped type 2// These are equivalent: 3 4type RecordVersion = Record<'a' | 'b', number>; 5 6type MappedVersion = { 7 [K in 'a' | 'b']: number; 8}; 9 10// Both result in: 11// { a: number; b: number; } 12 13// Record is simpler for basic cases 14// Mapped types allow more complex transformations

Partial Record#

1// All keys optional 2type PartialStatusMap = Partial<Record<Status, string>>; 3 4const partial: PartialStatusMap = { 5 pending: 'Waiting', 6 // active and completed are optional 7}; 8 9// Or use Partial inline with Record 10type OptionalUserMap = Record<string, User | undefined>; 11 12const maybeUsers: OptionalUserMap = { 13 'user-1': { id: 1, name: 'John', email: 'john@example.com' }, 14 'user-2': undefined, 15};

Record with Index Signature#

1// Record creates an index signature 2type StringRecord = Record<string, unknown>; 3 4// Same as: 5interface IndexSignature { 6 [key: string]: unknown; 7} 8 9// But Record is more flexible with key types 10type NumberRecord = Record<number, string>; 11 12const indexed: NumberRecord = { 13 0: 'zero', 14 1: 'one', 15 2: 'two', 16};

Type-Safe Configuration#

1// Define allowed config keys 2type ConfigKey = 'apiUrl' | 'timeout' | 'maxRetries' | 'debug'; 3 4// Values can be different types 5type ConfigValues = { 6 apiUrl: string; 7 timeout: number; 8 maxRetries: number; 9 debug: boolean; 10}; 11 12// Type-safe config object 13const config: ConfigValues = { 14 apiUrl: 'https://api.example.com', 15 timeout: 5000, 16 maxRetries: 3, 17 debug: false, 18}; 19 20// For uniform value types, use Record 21type FeatureFlags = Record<string, boolean>; 22 23const features: FeatureFlags = { 24 darkMode: true, 25 newUI: false, 26 betaFeatures: true, 27};

Lookup Tables#

1// HTTP status codes 2type HttpStatus = 200 | 201 | 400 | 401 | 404 | 500; 3 4type StatusMessages = Record<HttpStatus, string>; 5 6const statusMessages: StatusMessages = { 7 200: 'OK', 8 201: 'Created', 9 400: 'Bad Request', 10 401: 'Unauthorized', 11 404: 'Not Found', 12 500: 'Internal Server Error', 13}; 14 15function getStatusMessage(code: HttpStatus): string { 16 return statusMessages[code]; 17} 18 19// Type-safe route handlers 20type Routes = '/home' | '/about' | '/contact'; 21type RouteHandler = () => void; 22 23const handlers: Record<Routes, RouteHandler> = { 24 '/home': () => console.log('Home page'), 25 '/about': () => console.log('About page'), 26 '/contact': () => console.log('Contact page'), 27};

State Machines#

1// Define states 2type State = 'idle' | 'loading' | 'success' | 'error'; 3 4// Define transitions 5type Transitions = Record<State, State[]>; 6 7const validTransitions: Transitions = { 8 idle: ['loading'], 9 loading: ['success', 'error'], 10 success: ['idle', 'loading'], 11 error: ['idle', 'loading'], 12}; 13 14function canTransition(from: State, to: State): boolean { 15 return validTransitions[from].includes(to); 16} 17 18// State handlers 19type StateHandler<T> = (data: T) => void; 20type StateHandlers<T> = Record<State, StateHandler<T>>; 21 22const handlers: StateHandlers<string> = { 23 idle: () => console.log('Idle'), 24 loading: () => console.log('Loading...'), 25 success: (data) => console.log('Success:', data), 26 error: (error) => console.log('Error:', error), 27};

Translation/i18n#

1type Language = 'en' | 'es' | 'fr' | 'de'; 2 3type TranslationKey = 'greeting' | 'farewell' | 'thanks'; 4 5type Translations = Record<Language, Record<TranslationKey, string>>; 6 7const translations: Translations = { 8 en: { 9 greeting: 'Hello', 10 farewell: 'Goodbye', 11 thanks: 'Thank you', 12 }, 13 es: { 14 greeting: 'Hola', 15 farewell: 'Adiós', 16 thanks: 'Gracias', 17 }, 18 fr: { 19 greeting: 'Bonjour', 20 farewell: 'Au revoir', 21 thanks: 'Merci', 22 }, 23 de: { 24 greeting: 'Hallo', 25 farewell: 'Auf Wiedersehen', 26 thanks: 'Danke', 27 }, 28}; 29 30function t(lang: Language, key: TranslationKey): string { 31 return translations[lang][key]; 32}

Event Handlers#

1// DOM event types 2type EventType = 'click' | 'mouseover' | 'keydown'; 3 4type EventHandler = (event: Event) => void; 5 6type EventHandlers = Record<EventType, EventHandler>; 7 8const handlers: EventHandlers = { 9 click: (e) => console.log('Clicked', e), 10 mouseover: (e) => console.log('Mouseover', e), 11 keydown: (e) => console.log('Keydown', e), 12}; 13 14// Or with generics 15type TypedEventHandlers = { 16 [K in EventType]: (event: K extends 'keydown' ? KeyboardEvent : MouseEvent) => void; 17};

API Response Mapping#

1// Map endpoints to response types 2interface User { 3 id: number; 4 name: string; 5} 6 7interface Post { 8 id: number; 9 title: string; 10} 11 12type Endpoints = { 13 '/users': User[]; 14 '/users/:id': User; 15 '/posts': Post[]; 16 '/posts/:id': Post; 17}; 18 19// Generic fetch function 20async function fetchApi<K extends keyof Endpoints>( 21 endpoint: K 22): Promise<Endpoints[K]> { 23 const response = await fetch(endpoint); 24 return response.json(); 25} 26 27// Type-safe usage 28const users = await fetchApi('/users'); // User[] 29const user = await fetchApi('/users/:id'); // User

Record with keyof#

1interface User { 2 id: number; 3 name: string; 4 email: string; 5} 6 7// Create record from interface keys 8type UserValidation = Record<keyof User, (value: unknown) => boolean>; 9 10const validators: UserValidation = { 11 id: (v) => typeof v === 'number' && v > 0, 12 name: (v) => typeof v === 'string' && v.length > 0, 13 email: (v) => typeof v === 'string' && v.includes('@'), 14}; 15 16// Validate user 17function validateUser(user: unknown): user is User { 18 if (typeof user !== 'object' || user === null) return false; 19 20 return (Object.keys(validators) as (keyof User)[]).every( 21 (key) => validators[key]((user as any)[key]) 22 ); 23}

Best Practices#

When to Use Record: ✓ Dictionary/map structures ✓ Lookup tables ✓ Configuration objects ✓ Known set of keys Patterns: ✓ Union types as keys ✓ Enum values as keys ✓ keyof for interface keys ✓ Combine with Partial Type Safety: ✓ All keys required by default ✓ Use Partial for optional keys ✓ Leverage autocomplete ✓ Exhaustiveness checking Avoid: ✗ Using 'any' as key type ✗ Overcomplicating simple objects ✗ Ignoring undefined values ✗ Mixing with index signatures

Conclusion#

Record is a powerful utility type for creating type-safe dictionaries and mappings. Use it with union types or enums for exhaustive key checking, combine with Partial for optional keys, and leverage it for configuration objects, lookup tables, and state machines. It provides better type inference and autocomplete compared to plain index signatures.

Share this article

Help spread the word about Bootspring