Record<K, V> is a built-in utility type for an object whose keys have type K and whose values all have type V. With string keys it describes a dictionary; with a union of literal keys it describes an object that must have exactly those keys.
Record exists only in the type system. At runtime both objects are plain JavaScript objects, so they work with object literals, spread, JSON.stringify and everything else that takes an object.
Syntax and Definition
Record<Keys, Value>
Keys must be something that can be an object key: string, number, symbol, a union of string or number literals, or a template literal type. Value can be any type. The whole definition in TypeScript's standard library is one line, a mapped type:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
keyof any is string | number | symbol, the set of all possible key types. [P in K]: T creates one property of type T for each member of K. That explains both behaviors below: a wide K like string produces an index signature (any key), and a union K produces one required property per member.
Union Keys: Every Key Is Required
When the keys are a union of literals, a Record must list every one of them, and no others. This turns the compiler into a checklist:
Add "cancelled" to Status and both objects stop compiling until you give the new status a label and a color. Leaving a key out, or adding one that is not in the union, is a compile error:
index.ts(4,7): error TS2741: Property 'error' is missing in type '{ idle: string; loading: string; success: string; }' but required in type 'Record<Status, string>'.
index.ts(11,54): error TS2353: Object literal may only specify known properties, and 'paused' does not exist in type 'Record<Status, string>'.
The same works with a string enum as the key type: Record<Color, string> requires one entry per enum member.
Record<string, T> and the Missing Key
With string keys, any key is allowed, and TypeScript types every lookup as T, even for a key that is not there. At runtime a missing key gives undefined:
This is the most common Record bug. Three ways to handle it: check with in or Object.hasOwn before reading, declare the value as V | undefined, or turn on the noUncheckedIndexedAccess compiler option, which adds | undefined to every index-signature read in the project. A Record with union keys does not have this problem, because every key is guaranteed to exist.
Partial<Record<K, V>>: Only Some Keys
To use a union of keys but not require all of them, wrap the Record in Partial. Reads then return V | undefined, which is honest:
A misspelled key such as jp is still an error, which is the advantage over Record<string, string>.
Iterating over a Record
Object.keys, Object.values and Object.entries all work. The catch is the key type: Object.keys returns string[] and Object.entries returns [string, V][], never your union of keys:
TypeScript keeps the keys as string on purpose: an object can have more properties at runtime than its type lists, so promising Plan[] would be unsafe in general. For an object you created from a literal, like seats, the cast is safe.
Building a Record from Data
Records are the usual result of grouping or indexing an array. Start from an empty object with the Record type and fill it:
Book["genre"] reuses the union from the interface as the key type, so adding a genre to Book makes byGenre demand a new entry.
Record<string, unknown> and Interfaces
Record<string, unknown> is a common type for "some object with string keys". It accepts object literals and values typed with a type alias, but an interface is rejected:
index.ts(11,11): error TS2345: Argument of type 'User' is not assignable to parameter of type 'Record<string, unknown>'.
Index signature for type 'string' is missing in type 'User'.
Interfaces can be extended by declaration merging, so TypeScript does not assume they fit an index signature; type aliases cannot be reopened, so a type User = { name: string } would pass. The usual fixes are to accept object instead (you can still call Object.keys on it), make the function generic (<T extends object>(obj: T)), or declare User with type (the interface vs type page covers the other differences).
Record vs Index Signature vs Map
Record<K, V> | { [key: string]: V } | Map<K, V> | |
|---|---|---|---|
| Exists at runtime | no, a plain object | no, a plain object | yes, a class |
| Fixed set of keys | yes, with a union K | no | no |
| Key types | string, number, symbol, literal unions, template patterns | string, number, symbol, template patterns | anything, including objects |
| Missing key lookup type | V (with string keys) | V | V | undefined from get |
| Mix with named properties | via intersection & | yes, in the same type | no |
| JSON and spread | yes | yes | no, convert first |
| Size | Object.keys(r).length | Object.keys(o).length | m.size |
| Frequent add and delete | works | works | designed for it |
Pick Record with union keys whenever the set of keys is known: it is the only option that checks every key is present. For open-ended string keys, Record<string, V> and an index signature are interchangeable, and many codebases prefer Record for readability. Reach for a Map when keys are added and removed at runtime, when keys are not strings, or when you need the size and insertion order without extra work.
Frequently Asked Questions
What is Record in TypeScript?
Record<K, V> is a built-in utility type for an object whose keys are of type K and whose values are all of type V. Record<string, number> is an object with any string keys and number values; Record<"en" | "de", string> is an object with exactly the keys en and de, both strings.
What is the difference between Record and Map in TypeScript?
Record is a type for a plain JavaScript object, so it works with object literals, JSON and spread, and it disappears at compile time. Map is a runtime class with get, set, has and size, keeps insertion order for all keys, accepts any key type (objects too), and get returns V | undefined. Use a Record for fixed or JSON-shaped data and a Map for keys added and removed at runtime.
What is the difference between Record<string, T> and { [key: string]: T }?
For values they are the same type: Record<string, T> expands to an object type with a string index signature. Two small differences: an index signature can carry a name and sit next to other properties in the same type, and keyof Record<string, T> is string while keyof { [key: string]: T } is string | number.
How do I iterate over a Record in TypeScript?
Use Object.entries(record) for key-value pairs, Object.keys for keys and Object.values for values. The keys come back as string, not as K, because an object can hold extra keys at runtime. When the Record has a union of known keys, cast: (Object.keys(r) as Array<keyof typeof r>).
How do I make only some keys of a Record required?
With union keys, Record<K, V> requires every key. Wrap it in Partial to make all of them optional: Partial<Record<Lang, string>>. For a mix, intersect: Record<"en", string> & Partial<Record<"de" | "fr", string>> requires en and allows the others.