Skip to content
TypeScript ts utility-types 3 min read

Record

Record<K, V> constructs an object type whose property keys are K and whose property values are V. It is the idiomatic way to type dictionaries, lookup tables, and maps-as-objects, and it is far more expressive than a bare index signature because K can be a finite union of literals. When K is a union of string literals, Record even forces you to provide every key — a built-in exhaustiveness guarantee. It is implemented as a one-line mapped type.

Basic dictionaries

Use Record<string, V> when keys are arbitrary strings and every value shares one type — the equivalent of { [key: string]: V } but more readable.

type ScoreBoard = Record<string, number>;

const scores: ScoreBoard = {
  ada: 42,
  linus: 37,
};

scores.grace = 50; // ✅ any string key allowed

This is the go-to type for caches, counters, and id-to-entity maps where the key set is open-ended.

Constrained, exhaustive keys

The real power appears when K is a union of literals. TypeScript then requires all keys to be present, turning Record into an exhaustive lookup.

type Role = "admin" | "editor" | "viewer";

const permissions: Record<Role, string[]> = {
  admin: ["read", "write", "delete"],
  editor: ["read", "write"],
  viewer: ["read"],
  // ❌ Error if any role is missing
};

If you later add "owner" to Role, the permissions object fails to compile until you handle it — an exhaustiveness check for free. See discriminated unions for related patterns.

How it is built

Record is the simplest of the mapped utility types:

type MyRecord<K extends keyof any, V> = {
  [P in K]: V;
};

keyof any resolves to string | number | symbol, the set of all valid property-key types. Mapping over K assigns V to every key. When K is a literal union, each member becomes a concrete property; when it is string, the result collapses to an index signature.

Record vs index signature

FeatureRecord<K, V>{ [k: string]: V }
Literal-union keysyes — enforces all keysno
Number/symbol keysyesonly one key type at a time
Reads as intent”dictionary of V”structural index
Exhaustivenessyes with literal Kno

For finite key sets always prefer Record with a literal union; reserve index signatures for genuinely open maps with mixed declared members.

Combining with keyof

Pairing Record with keyof lets you derive a lookup keyed by another type’s properties:

interface Settings {
  theme: string;
  fontSize: number;
}

// A validator for each setting key
type Validators = Record<keyof Settings, (value: unknown) => boolean>;

const validators: Validators = {
  theme: (v) => typeof v === "string",
  fontSize: (v) => typeof v === "number",
};

This guarantees the validators object stays in sync with Settings — add a property and the validator map must follow.

A caveat with open keys

When you use Record<string, V>, every access is typed as V, even for keys that may not exist at runtime:

const cache: Record<string, number> = {};
const value = cache.missing; // typed as number, but actually undefined!

Enable noUncheckedIndexedAccess in tsconfig so indexed reads on open records return V | undefined, forcing you to handle the missing case safely.

Best Practices

  • Prefer Record<K, V> over index signatures for readability and literal-key exhaustiveness.
  • Use a literal union for K whenever the key set is finite — it catches missing entries at compile time.
  • Enable noUncheckedIndexedAccess so open Record<string, V> reads are V | undefined.
  • Derive value maps from existing types with Record<keyof T, V> to keep them synchronized.
  • For typed runtime maps with non-string keys or iteration, consider a real Map<K, V> instead.
  • Avoid Record<string, any>; pick a precise value type or use unknown and narrow.
Last updated June 29, 2026
Was this helpful?