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 transformationsPartial 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'); // UserRecord 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.