Record Utility Type in TypeScript
Record builds an object type where every key has the same value type. Use it for lookup maps, dictionaries, and any object with a known set of keys.
Record<K, V> is a built-in utility type that builds an object type from a set of keys and a value type. Every key in the set maps to the same value type. The result is a type where the compiler knows exactly which keys exist and what type each value has.
This is the standard way to type lookup maps, dictionaries with known keys, and configuration objects keyed by a fixed set of identifiers.
type Role = "admin" | "editor" | "viewer";
const permissions: Record<Role, string[]> = {
admin: ["create", "read", "update", "delete"],
editor: ["create", "read", "update"],
viewer: ["read"],
};
console.log(permissions.editor);
// Output:
// ["create", "read", "update"]The type Record<Role, string[]> says: this object must have exactly the keys admin, editor, and viewer, and each value must be a string array. If you miss a role or mistype a key, the compiler catches it. If you access permissions.guest, the compiler tells you guest does not exist.
How Record works
Record takes two type arguments. The first is the set of keys, usually a union of string literals. The second is the type that every value must match:
type Status = "loading" | "success" | "error";
type StatusMessages = Record<Status, string>;
const messages: StatusMessages = {
loading: "Fetching data...",
success: "Data loaded.",
error: "Something went wrong.",
};Every key from the Status union must appear in the object. Every value must be a string. The compiler enforces both constraints. If you add "idle" to the Status union later, every Record<Status, ...> in your codebase will produce a missing-property error until you add the "idle" entry.
Record for enum-like lookups
A common pattern is using Record to build a lookup table from a set of identifiers to display labels or configuration:
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
const methodLabels: Record<HttpMethod, string> = {
GET: "Retrieve data",
POST: "Create resource",
PUT: "Update resource",
DELETE: "Remove resource",
};The compiler guarantees that every HTTP method has a label, because Record requires all four keys to be present. A helper function can now look up a label for any method without ever handling a missing case:
function describeMethod(method: HttpMethod): string {
return methodLabels[method];
}
console.log(describeMethod("POST"));
// Output:
// Create resourceIf a new method is added to the HttpMethod union, the labels object must be updated too, or the compiler reports a missing property error. This makes Record a reliable way to enforce exhaustiveness in lookup tables.
Record with complex value types
The value type can be any TypeScript type, including interfaces, unions, arrays, or even other Records:
interface ProductInfo {
price: number;
inStock: boolean;
}
type ProductId = "widget" | "gadget" | "doohickey";Each product id now maps to a full ProductInfo object instead of a plain string. The catalog below must supply price and inStock for all three ids, and every value must satisfy the ProductInfo shape:
const catalog: Record<ProductId, ProductInfo> = {
widget: { price: 9.99, inStock: true },
gadget: { price: 24.99, inStock: false },
doohickey: { price: 4.99, inStock: true },
};
console.log(catalog.widget.price);
// Output:
// 9.99Each product entry has the same shape because Record enforces a uniform value type. If gadget needs an extra field like weight, you must add it to ProductInfo first. This keeps your data model consistent and prevents one-off field additions that other code paths might not handle.
Record vs index signatures
Record and index signatures both create object types, but they serve different purposes:
| Feature | Record<K, V> | Index signature { [key: string]: V } |
|---|---|---|
| Keys | Exact set (union of literals) | Any string or number |
| Missing keys | Compiler error | Allowed (returns V | undefined) |
| Autocomplete | Full on known keys | None on arbitrary keys |
| Exhaustiveness | Required | Not applicable |
Here is the difference in practice:
// Record: exact keys, all required
type Config = Record<"host" | "port" | "timeout", string>;
const c1: Config = { host: "localhost", port: "3000", timeout: "5000" };
c1.host; // Autocomplete: "host" | "port" | "timeout"
// Index signature: any string key, values may be absent
type LooseConfig = { [key: string]: string };
const c2: LooseConfig = { host: "localhost", port: "3000" };
c2.timeout; // Type is string | undefined, no autocompleteUse Record when you know the exact keys and every key is required. Use an index signature when the keys are dynamic or open-ended, like HTTP headers or query parameters where you cannot know all possible keys at compile time.
For more on index signatures, see index signatures in TypeScript.
Record with Partial for optional keys
Sometimes the keys are known but not all of them are required. Combine Record with Partial utility type in TypeScript:
type Feature = "darkMode" | "notifications" | "analytics";
type FeatureFlags = Partial<Record<Feature, boolean>>;
const flags: FeatureFlags = {
darkMode: true,
// notifications and analytics are not set yet
};
if (flags.notifications) {
console.log("Notifications are on.");
}Partial<Record<Feature, boolean>> makes every feature flag optional. You can set some flags now and add others later. The compiler still knows the exact set of possible keys, so autocomplete works and typos are caught.
Common mistakes
Using Record when an index signature is simpler. If your keys are unknown at compile time, such as user-generated tags or dynamic query parameters, use an index signature instead. Record forces you to list every key, which is impossible for open-ended key sets.
Forgetting that Record requires every key to be present. Record<"a" | "b" | "c", number> demands all three keys. If you only want some, use Partial or make the value type include undefined. Otherwise the compiler will reject objects with missing keys.
Using Record for heterogeneous value types. If each key needs a different value type, Record is the wrong tool. Record enforces a uniform value type across all keys. For heterogeneous shapes, use a regular interface or type alias where each property gets its own type annotation.
Rune AI
Key Insights
⚠ This article has a formatting issue and may not display correctly.
Our team has been notified. The content is shown as plain text below.
Frequently Asked Questions
What is the difference between Record and an index signature?
Can I use a union of numbers as keys in Record?
What happens if I use Record with a union type for the value?
Conclusion
Record is the go-to type for objects that map known keys to uniform values. Use it for lookup tables, configuration maps keyed by feature names, and any data structure where the keys are known in advance and the values share a type. For open-ended key sets, use an index signature instead.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.