Record<Keys, Type>
Builds an object type whose keys come from a union and whose values all share one type.
type Record<Keys extends keyof any, Type> = { [Property in Keys]: Type }- Added in TypeScript 2.1
Parameters
| Parameter | Type | Description |
|---|---|---|
$Keys | keyof any (string | number | symbol, or a union of literals thereof) | The property keys of the resulting object type. A union of string literals produces a fixed set of required keys; the wide `string` produces an index signature instead. |
$Type | any type | The type shared by every property value. |
Return value
An object type with one property per member of Keys, each of type TypeA mapped type with exactly the keys in Keys, every value typed as Type.
Errors
The checker errors if the object literal omits a key from a literal union, or adds one that is not in it.
Changelog
- PHP 4.1Key remapping with `as` in mapped types arrived, giving a way to derive transformed key sets that Record's fixed mapping cannot express.
- PHP 4.4`noUncheckedIndexedAccess` makes lookups on a `Record<string, T>` produce `T | undefined`, which is the correct behaviour but breaks code that assumed every lookup hit.
Record is the mapped type you use when the keys are known and the values all share a shape. It has two quite different modes depending on what you pass as Keys.
Literal union keys: an exhaustive table
Record<"debug" | "info" | "error", number> produces exactly three required properties. Forgetting one is a compile error, and adding a fourth is a compile error. This is the valuable mode: derive the union from an existing type (or a discriminated union's tag) and the checker will force you to update the table whenever a new member is added.
Wide keys: an index signature
Record<string, number> is just { [key: string]: number }. Nothing is required and nothing is checked, so it is a convenient way to say "dictionary" and a poor way to get safety.
What trips people up
Lookups lie by default. On a Record<string, T>, table["missing"] is typed T but is undefined at runtime. Turn on noUncheckedIndexedAccess so it types as T | undefined.
Numeric keys become strings. Record<1 | 2, string> is fine in the type system, but Object.keys() returns ["1", "2"] — string keys, as always in JavaScript.
Example
When to use it
- Map each member of a string-literal union or enum to a handler, label, or numeric weight.
- Force a translations or config table to stay exhaustive as new union members are added.
- Type a plain-object cache or counter keyed by arbitrary strings.
Frequently asked questions
What is the difference between Record<string, T> and an index signature?
There is none — `Record<string, T>` expands to `{ [key: string]: T }`. The difference in practice is that `Record` with a union of literal keys produces a fixed, exhaustive set of required properties instead of an open-ended signature.
How do I create a dictionary type in TypeScript?
Use `Record<string, ValueType>` for an open dictionary, or `Record<"a" | "b", ValueType>` when the keys are known ahead of time so the checker enforces that all of them are present.
Why does my lookup return undefined when the type says it cannot?
Because an index signature claims every key exists. Enable the `noUncheckedIndexedAccess` compiler option so indexed reads are typed `T | undefined`, or key the record on a literal union where every property is genuinely present.
More examples
An exhaustive handler table for a discriminated union
Prints `{ count: 5, label: 'hits' }`. The point is what happens when someone adds a fourth `Action` variant: `Action["type"]` gains a member, the `Record` gains a required key, and the object literal stops compiling until a handler is written for it — an exhaustiveness check with no switch statement.
type Action =
| { type: "increment"; by: number }
| { type: "reset" }
| { type: "setLabel"; label: string };
type State = { count: number; label: string };
// Keys are derived from the union, so a new Action member breaks the build here.
const handlers: Record<Action["type"], (s: State, a: any) => State> = {
increment: (s, a) => ({ ...s, count: s.count + a.by }),
reset: (s) => ({ ...s, count: 0 }),
setLabel: (s, a) => ({ ...s, label: a.label }),
};
function reduce(state: State, action: Action): State {
return handlers[action.type](state, action);
}
let state: State = { count: 0, label: "counter" };
state = reduce(state, { type: "increment", by: 5 });
state = reduce(state, { type: "setLabel", label: "hits" });
console.log(state);Wide keys and the missing-lookup problem
The first log prints `value: undefined typeof: undefined` even though the declared type says `number` — the index signature promises a value for every string key and cannot deliver. With `noUncheckedIndexedAccess` enabled, that same line would be typed `number | undefined` and the `?? 0` fallback becomes mandatory. The final line sorts the keys so the output is stable across runs.
const counts: Record<string, number> = { apples: 3, pears: 1 };
// Typed as number, but there is no such key.
const bananas = counts["bananas"];
console.log("value:", bananas, "typeof:", typeof bananas);
// Safe patterns:
console.log("with fallback:", counts["bananas"] ?? 0);
console.log("guarded:", Object.hasOwn(counts, "bananas"));
const total = Object.keys(counts)
.sort()
.map((k) => `${k}=${counts[k]}`)
.join(", ");
console.log(total);
Discussion