Menu

TypeScript Record Type: Record<K, V> Explained with Examples

Record<K, V> is the object type whose keys are K and whose values are all V. Learn Record with string keys and with union keys (every key required), Partial<Record>, looping over a Record with typed keys, the missing-key pitfall, and when to use an index signature or a Map instead.

This page includes runnable editors - edit, run, and see output instantly.

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 runtimeno, a plain objectno, a plain objectyes, a class
Fixed set of keysyes, with a union Knono
Key typesstring, number, symbol, literal unions, template patternsstring, number, symbol, template patternsanything, including objects
Missing key lookup typeV (with string keys)VV | undefined from get
Mix with named propertiesvia intersection &yes, in the same typeno
JSON and spreadyesyesno, convert first
SizeObject.keys(r).lengthObject.keys(o).lengthm.size
Frequent add and deleteworksworksdesigned 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.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED