The TypeScript Record Type, Explained With Your Own Keys

Record<K, V> is a built-in utility type for an object whose keys come from K and whose values all have type V. Give it a union of string literals and the compiler insists every listed key is present. Give it plain string and it behaves like an open dictionary, the same as an index signature.

Record at a glance
You writeMeansMissing key allowed?
Record<"a" | "b", number>{ a: number; b: number }No, both keys required
Record<string, number>{ [key: string]: number }Yes, any string key
Partial<Record<"a" | "b", number>>{ a?: number; b?: number }Yes, each key optional
Record<number, string>{ [key: number]: string }Yes, any number key

Updated

Type the keys you want and pick a value type. The builder writes the Record type, the plain object type it expands to, and a starter object with one line per key, so you can see exactly what the compiler will hold you to before you paste it into your code.

Record type builder

type PageKey = "home" | "about" | "contact";

type Pages = Record<PageKey, string>;
type Pages = {
  home: string;
  about: string;
  contact: string;
};
const pages: Pages = {
  home: "",
  about: "",
  contact: "",
};

Patterns you will use most

Keys from an as const array

The array is the single source of truth. Add a status to it and the compiler points at the label table until you add the label too.

const STATUSES = ["draft", "live", "archived"] as const;
type Status = (typeof STATUSES)[number]; // "draft" | "live" | "archived"

const statusLabel: Record<Status, string> = {
  draft: "Draft",
  live: "Published",
  archived: "Archived",
};

One flag per field of another type

keyof turns an existing type into the key union, so a form's touched-state object always lists the same fields as the form.

interface SignupForm {
  email: string;
  password: string;
  name: string;
}

type Touched = Record<keyof SignupForm, boolean>;

const touched: Touched = { email: false, password: false, name: false };

An open dictionary, read safely

With noUncheckedIndexedAccess on, a read by a computed key is number | undefined, so the fallback is required rather than forgotten.

const visitsByPath: Record<string, number> = {};

function countVisit(path: string) {
  const current = visitsByPath[path] ?? 0;
  visitsByPath[path] = current + 1;
}

How Record is built

Record is a mapped type. It walks every member of the key type and gives each one the same value type, which is why Record<"a" | "b", number> expands to an object type with two required number properties.

Because it is just a type, you can feed it anything that can be a property key: string literals, number, string, symbol, or a union of those. A key type built from another type, such as keyof User, updates itself when User changes.

Closed keys versus open keys

The value of Record is the closed form. A lookup table of route names, status labels or theme colours typed with a union of literals breaks the build the day someone adds a new status without a label, which is exactly when you want to hear about it.

The open form, Record<string, T>, describes a dictionary. It says nothing about which keys exist, so reading a key gives T even when the key is absent. Pair it with noUncheckedIndexedAccess or a Map when that gap matters.

Record, index signature or Map

Pick Record with literal keys for fixed lookups, Record<string, T> or an index signature for JSON-shaped dictionaries, and Map for collections that change while the program runs. The TypeScript Map guide on this site covers the last case in depth.

How it works

  1. Type your keys separated by commas, for example home, about, contact.
  2. Choose the value type each key should hold.
  3. Tick optional if some keys may be left out, which wraps the type in Partial.
  4. Copy the type and the starter object into your file and fill in the values.

Tips

Frequently asked questions

Is Record the same as an index signature?

With a wide key such as string, yes: Record<string, number> and { [key: string]: number } describe the same shape. The difference shows with literal keys. An index signature cannot demand that specific names exist, while Record<"a" | "b", number> requires both a and b.

When should I use a Map instead of a Record?

Use a Map when keys are added and removed often, when keys are not strings, or when you need the entry count through size. Use a Record for a fixed set of names, for config objects and for anything you send through JSON.stringify.

Why does TypeScript say a property is missing from my Record?

Because the key type is a union and the object leaves one member out. tsc reports, for example, Property 'about' is missing in type '{ home: string; }'. Add the key, or wrap the type in Partial if leaving it out is valid.

Does Record exist at runtime?

No. It is a type only and disappears when the code is compiled. The object you create is an ordinary JavaScript object, so the compiler cannot stop a server response from having extra or missing keys.

Which TypeScript version added Record?

Record shipped in TypeScript 2.1, alongside Partial, Readonly and Pick, so any current compiler has it.

Related: TypeScript Map, TypeScript interfaces, TypeScript data types, TypeScript enums and alternatives, JSON to TypeScript converter.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap