TypeScriptRecord

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

TS
// 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"]); // 95
Note
Record<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:

TS
// 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:

TS
// 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:

TS
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 type
Success
When K 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:

TS
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:

TS
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")); // false
Dictionaries 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:

TS
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"
Tip
When using 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:

TS
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); // 2
Record in Generic Utilities

Record is very composable with other utility types and generics:

TS
// 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

TS
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.

Warning
If you access a 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

Success
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.