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
| Feature | Record<K, V> | { [k: string]: V } |
|---|---|---|
| Literal-union keys | yes — enforces all keys | no |
| Number/symbol keys | yes | only one key type at a time |
| Reads as intent | ”dictionary of V” | structural index |
| Exhaustiveness | yes with literal K | no |
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
noUncheckedIndexedAccessin tsconfig so indexed reads on open records returnV | 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
Kwhenever the key set is finite — it catches missing entries at compile time. - Enable
noUncheckedIndexedAccessso openRecord<string, V>reads areV | 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 useunknownand narrow.