skip to content

In TypeScript, what object type does the built-in `Record<K, V>` utility produce, and how does its mapped-type definition explain the difference between `Record<string, number>` and `Record<'a' | 'b', number>`?

level: juniorimportance: must knowfreq 72%

answer

  1. one line in the standard library
  2. a mapped type over the key set
  3. open key set versus finite key set
  4. index signature versus named required properties
  5. nothing survives compilation

basics

~20 s

Record<K, V> builds an object type whose keys come from K and whose values are all V. It is defined as the mapped type { [P in K]: V }, so a string key produces an open index signature while a union of literals produces exactly those required properties.

solid answer

~50 s

`Record<K, V>` is a one-line mapped type in the standard library: `type Record<K extends keyof any, T> = { [P in K]: T }`. The `[P in K]` part walks over every member of `K` and gives each one the value type `T`, which is why the two parameters read as "keys" and "values". What you get back depends entirely on what `K` is. With an open key type, `Record<string, number>` expands to `{ [x: string]: number }` — any string key is allowed and none is required. With a union of string literals, `Record<'a' | 'b', number>` expands to `{ a: number; b: number }` — two named, required properties, so a literal missing `b` is an error and one with an extra `c` trips the excess-property check. Like everything in the type layer it is erased: no `Record` exists in the emitted JavaScript.

code

typescript · 12 lines
typescript
// Finite key set -> named, required properties
type Flags = Record<'read' | 'write', boolean>;
const flags: Flags = { read: true, write: false };

// Open key set -> an index signature
type Scores = Record<string, number>;
const scores: Scores = {};
scores.anything = 1;

// Optional entries need an explicit wrapper
type MaybeFlags = Partial<Record<'read' | 'write', boolean>>;
const partial: MaybeFlags = { read: true };

go deeper

for a junior

Be able to read Record<K, V> aloud as keys of type K, values of type V, and to say that a union-of-literals key gives you those exact required properties while a string key gives you an open dictionary.

for a middle

Explain the definition { [P in K]: T } and derive both behaviours from it: a finite key set becomes named required properties, an open key type becomes an index signature. Mention that Partial is how you make the entries optional.

for a senior

Show judgment about when a uniform value type is the wrong model, and flag that a Record annotation on parsed JSON asserts a shape nothing validated. Know that string-keyed reads are typed as present even when the key is absent.

for a principal

Own the guidance for a codebase: which lookup shapes are Record, which are interfaces, and where a computed key set is worth the type-level indirection versus a plain listed type that a newcomer can read at a glance.

## Record is a type alias, not compiler magic `Record` is written in TypeScript's own standard library (`lib.es5.d.ts`) as a single mapped type: ```ts type Record<K extends keyof any, T> = { [P in K]: T; }; ``` Everything about its behaviour falls out of that one line. `keyof any` is the set of legal property key types — `string | number | symbol` — so the constraint on `K` just says "whatever you pass must be usable as a key". `T` is unconstrained: values can be primitives, objects, functions, unions, anything. The body `{ [P in K]: T }` is a **mapped type**. Read it as a loop: for each member `P` of the key type `K`, declare a property named `P` whose type is `T`. That is why people describe `Record` as "keys of type K, values of type V" — the loop assigns the same value type to every generated key. ## The two modes, and why they differ The interesting behaviour is that the *same* utility produces two very different-feeling types depending on how open `K` is. **Open key type.** `Record<string, number>` loops over `string`, which is not a finite set of names, so the compiler produces an index signature instead of named properties: ```ts type Scores = Record<string, number>; // { [x: string]: number } const s: Scores = {}; // fine — no key is required s.anything = 1; // fine — any string key is allowed const n: number = s.whatever; // compiles, even though the key is absent ``` Nothing is required and nothing is forbidden. `Record<number, T>` behaves the same way with a numeric index signature, and `Record<symbol, T>` with a symbol one. **Closed key type.** A union of string literals *is* a finite set, so the loop produces one named property per member: ```ts type Flags = Record<'read' | 'write', boolean>; // { read: boolean; write: boolean } const ok: Flags = { read: true, write: false }; const missing: Flags = { read: true }; // Error: Property 'write' is missing const extra: Flags = { read: true, write: false, exec: true }; // Error: Object literal may only specify known properties ('exec' does not exist) ``` Each generated property is **required** — the mapped type adds no `?` modifier. If you want the partial version, wrap it: `Partial<Record<'read' | 'write', boolean>>` makes every entry optional. The same applies to enums, which are also finite key sets: `Record<Color, string>` demands an entry for every member of `Color`. ## Reading a value out For a literal-keyed `Record`, property access is ordinary property access: `flags.read` is `boolean`, and misspelling the key is a compile error because no such property exists. For a string-keyed `Record`, `scores[k]` is typed `number` under default settings even when the key is absent at runtime — the index signature is a promise the checker takes at face value. That gap is the single most important thing to know about `Record<string, T>`, and it is what pushes people to model absent entries explicitly with a `| undefined` value type. ## Two small typing details that surprise people `keyof Record<string, T>` is `string | number`, not just `string` — a numeric key is usable against a string index signature, so both are reported. And `Object.keys(rec)` is typed `string[]` by the standard library regardless of how narrow the `Record` key type is, so iterating a `Record<'a' | 'b', number>` does not hand you `('a' | 'b')[]` for free; you have to assert or write a helper if you want the narrow key type. ## It is erased `Record` is part of the type layer only. Compiling ```ts type Flags = Record<'read' | 'write', boolean>; const f: Flags = { read: true, write: false }; ``` emits just the object literal. There is no runtime construct that validates the keys, no check that the values are booleans, and nothing to import. If the data comes from JSON or a network response, the `Record` annotation asserts a shape that only a runtime validation step can actually guarantee. ## When to reach for it Use `Record` when the value type is uniform and the keys are the interesting part: a lookup table from a status to a label, a map from a feature key to a handler, a bag of headers. When different keys need different value types, `Record` is the wrong tool — that is an interface or a discriminated shape, because forcing one value type across heterogeneous fields means widening it to a union and losing the precision at every read site.

  • What does `Record<K, V>` give you that writing the object type by hand does not?
    Mostly reuse and composition. Because it is a generic alias, the key set can be computed — a union, an enum, `keyof SomeType`, a template-literal type — and it composes with other utilities, so `Partial<Record<Status, Handler>>` or `Readonly<Record<string, Config>>` are one-liners. Written by hand you would restate the key list at every site, and the two copies drift apart the moment a key is added.
  • If the keys and value types are not uniform, is `Record` still the right choice?
    No. `Record` assigns one value type to every key. Modelling heterogeneous fields with it forces the value type up to a union such as `string | number | Date`, so every read site has to narrow again before it can do anything useful. That is a signal to declare an interface with the real per-property types instead.
  • Does `Record<'a' | 'b', number>` make the entries optional or required?
    Required. The mapped type `{ [P in K]: T }` adds no optional modifier, so both `a` and `b` must be present, and an object literal missing one is a compile error. Wrap it as `Partial<Record<'a' | 'b', number>>` when the map is genuinely allowed to be sparse — that adds `?` to every generated property and makes each read `number | undefined`.

saying these in an interview costs you the question

  • Thinking Record does a runtime check on the object's keys
  • Claiming Record entries are optional by default
  • Saying Record only accepts string keys
  • Believing Record<string, T> requires at least one key to exist
  • Assuming Object.keys on a Record returns the narrow key union

context