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.
| You write | Means | Missing 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.
type PageKey = "home" | "about" | "contact";
type Pages = Record<PageKey, string>;type Pages = {
home: string;
about: string;
contact: string;
};const pages: Pages = {
home: "",
about: "",
contact: "",
};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",
};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 };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;
}
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.
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.
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.
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.
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.
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.
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.
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.
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.