Record<Keys, Type>

Builds an object type whose keys come from a union and whose values all share one type.

Signature
type Record<Keys extends keyof any, Type> = { [Property in Keys]: Type }
  • Added in TypeScript 2.1

Parameters

ParameterTypeDescription
$Keyskeyof 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.
$Typeany typeThe 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · typescript
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.

Example · typescript
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

  • Be the first to comment on this lesson.