Record
Record<K, V> is a utility type that constructs an object type whose keys are type K and whose values are type V. It is the idiomatic TypeScript way to express a dictionary, lookup table, or any map-like data structure.
Basic Syntax
// Record<Keys, Value>
type StringRecord = Record<string, number>;
// { [key: string]: number }
type UserMap = Record<string, { name: string; age: number }>;
// { [key: string]: { name: string; age: number } }
const scores: Record<string, number> = {
alice: 95,
bob: 87,
charlie: 92,
};
console.log(scores["alice"]); // 95Record<string, V> is equivalent to { [key: string]: V }. The utility type form is usually preferred for readability.How Record Is Implemented
Record is a mapped type that iterates over each member of K and assigns value type V:
// TypeScript's built-in definition
type Record<K extends keyof any, T> = {
[P in K]: T;
};
// K extends keyof any means K can be string, number, symbol,
// or a union of string/number literals.Record vs Index Signatures
Both Record and index signatures express "an object with values of a certain type", but they have different ergonomics:
// Index signature syntax
interface StringToNumber {
[key: string]: number;
}
// Record syntax
type StringToNumberRecord = Record<string, number>;
// They behave identically for string keys.
// Record shines when K is a union of literals:Feature | Index Signature | Record<K, V> |
|---|---|---|
Syntax | { [key: string]: V } | Record<string, V> |
Union literal keys | Not directly supported | Record<"a" | "b", V> — exhaustive |
Defined in interface | Yes | No (type alias only) |
Mixed key types | One index per type | K can be complex union |
Readability | Explicit but verbose | Concise for well-known patterns |
Using Union Types as Keys
The real power of Record over plain index signatures is using a finite union as the key type. This forces you to handle every possible key — making your code exhaustive and refactor-safe:
type Day = "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" | "Sun";
// Every day MUST have a value — TypeScript enforces completeness
const schedule: Record<Day, string> = {
Mon: "9am - 5pm",
Tue: "9am - 5pm",
Wed: "9am - 5pm",
Thu: "9am - 5pm",
Fri: "9am - 5pm",
Sat: "Closed",
Sun: "Closed",
};
// If you remove "Sun", TypeScript raises:
// Property 'Sun' is missing in typeK is a union of string literals, Record<K, V> acts as an exhaustiveness check — the compiler rejects any object literal that is missing a key.Lookup Tables
Lookup tables are one of the most practical uses of Record. Instead of switch statements or if/else chains, you map keys to values or functions:
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
const methodColors: Record<HttpMethod, string> = {
GET: "green",
POST: "blue",
PUT: "orange",
PATCH: "yellow",
DELETE: "red",
};
function getMethodColor(method: HttpMethod): string {
return methodColors[method];
}
console.log(getMethodColor("DELETE")); // "red"red
State Machine Transitions
A state machine maps each state to its allowed transitions. Record with a union key type is a natural fit:
type OrderStatus = "draft" | "pending" | "confirmed" | "shipped" | "delivered" | "cancelled";
// Maps each status to the statuses it can transition to
const transitions: Record<OrderStatus, OrderStatus[]> = {
draft: ["pending", "cancelled"],
pending: ["confirmed", "cancelled"],
confirmed: ["shipped", "cancelled"],
shipped: ["delivered"],
delivered: [],
cancelled: [],
};
function canTransition(from: OrderStatus, to: OrderStatus): boolean {
return transitions[from].includes(to);
}
console.log(canTransition("pending", "confirmed")); // true
console.log(canTransition("delivered", "cancelled")); // falseDictionaries with String Keys
When the keys are not known ahead of time (e.g., user-defined IDs), use string or number as the key type:
interface UserProfile {
name: string;
email: string;
avatarUrl: string;
}
// Keyed by user ID (string UUIDs)
const userCache: Record<string, UserProfile> = {};
function cacheUser(id: string, profile: UserProfile): void {
userCache[id] = profile;
}
function getUser(id: string): UserProfile | undefined {
return userCache[id];
}
cacheUser("abc-123", { name: "Alice", email: "alice@example.com", avatarUrl: "/avatars/alice.png" });
const user = getUser("abc-123");
console.log(user?.name); // "Alice"Record<string, V>, TypeScript cannot guarantee a key exists, so the value type is V. If you access a missing key you get undefined at runtime but V at the type level. Enable noUncheckedIndexedAccess in tsconfig to get V | undefined instead.Grouping and Indexing Arrays
A common pattern is grouping an array of items by some property. Record is the right return type:
interface Transaction {
id: string;
category: "food" | "travel" | "utilities" | "entertainment";
amount: number;
}
function groupByCategory(
transactions: Transaction[]
): Record<Transaction["category"], Transaction[]> {
const groups: Record<Transaction["category"], Transaction[]> = {
food: [],
travel: [],
utilities: [],
entertainment: [],
};
for (const tx of transactions) {
groups[tx.category].push(tx);
}
return groups;
}
const transactions: Transaction[] = [
{ id: "1", category: "food", amount: 12 },
{ id: "2", category: "travel", amount: 200 },
{ id: "3", category: "food", amount: 8 },
];
const grouped = groupByCategory(transactions);
console.log(grouped.food.length); // 2Record in Generic Utilities
Record is very composable with other utility types and generics:
// Map each key of an object to its value's type name
function getTypeMap<T extends object>(obj: T): Record<keyof T, string> {
const result = {} as Record<keyof T, string>;
for (const key in obj) {
result[key as keyof T] = typeof obj[key as keyof T];
}
return result;
}
const user = { id: 1, name: "Alice", active: true };
console.log(getTypeMap(user));
// { id: "number", name: "string", active: "boolean" }
// Invert an object (swap keys and values)
function invert<K extends string, V extends string>(
obj: Record<K, V>
): Record<V, K> {
const result = {} as Record<V, K>;
for (const [k, v] of Object.entries(obj) as [K, V][]) {
result[v] = k;
}
return result;
}
const statusCodes = invert({ ok: "200", notFound: "404", error: "500" });
console.log(statusCodes["200"]); // "ok"Combining Record with Other Utility Types
type Role = "admin" | "editor" | "viewer";
interface Permission {
canRead: boolean;
canWrite: boolean;
canDelete: boolean;
}
// Every role has a full permission object
const permissions: Record<Role, Permission> = {
admin: { canRead: true, canWrite: true, canDelete: true },
editor: { canRead: true, canWrite: true, canDelete: false },
viewer: { canRead: true, canWrite: false, canDelete: false },
};
// Partial permissions for overrides
type PermissionOverride = Record<Role, Partial<Permission>>;
// Read-only snapshot of the permission table
type FrozenPermissions = Readonly<Record<Role, Readonly<Permission>>>;Pitfalls to Avoid
Record<string, V> does not guarantee a key exists at runtime. Always check before accessing or enable noUncheckedIndexedAccess.
Do not use Record when you want some keys optional and others required — use an explicit interface instead.
Record<never, V> produces an empty object type — this is rarely useful.
Avoid Record<any, V> as the key type — it defeats type safety. Prefer string, number, or a literal union.
Record is always an object type — it cannot represent Map or Set data structures.
Record<string, V> with a key that was never set, you get undefined at runtime even though TypeScript says the type is V. Guard with if (key in obj) or use a Map if absence must be tracked explicitly.Record vs Map
Feature | Record<K, V> | Map<K, V> |
|---|---|---|
Type-safe keys | Yes (literal unions) | Yes (generics) |
Non-string keys | Only string/number/symbol | Any type (objects, arrays) |
Order guaranteed | No (object key order) | Yes (insertion order) |
JSON serializable | Yes (plain object) | No (needs Array.from) |
Size (.size) | No | Yes |
Iteration | Object.keys / Object.entries | .forEach / for...of |
Quick Reference
Pattern | Example | Use Case |
|---|---|---|
Record<string, V> | Record<string, User> | ID → object cache |
Record<LiteralUnion, V> | Record<Day, string> | Exhaustive lookup table |
Record<keyof T, string> | Record<keyof User, string> | Map every field to a description |
Partial<Record<K, V>> | Partial<Record<Role, string>> | Optional per-role values |
Readonly<Record<K, V>> | Readonly<Record<Status, Config>> | Immutable config table |
Record<K, V> is the go-to type for dictionaries and lookup tables. Use it with union literal keys for exhaustive compile-time checks, and with string or number keys for runtime-keyed caches and indexes.