TypeScript Record vs Map: Differences and When to Use Each

A Record types a plain object; a Map is a runtime collection. Compare them on keys, order, JSON, type checks and user input, and convert one into the other.

TypeScript Record vs Map is a choice between a compile-time type and a runtime class, even though both hold key-value data. The type Record<K, V> describes a plain JavaScript object whose keys have type K and values type V, and it disappears when the code is compiled. The class Map<K, V> stores key-value pairs in insertion order and has its own methods, such as set(), get(), has() and delete().

We pick a Record for a known set of string keys and for data that goes to or comes from JSON, such as the settings of a user profile. We pick a Map when keys come from user input or change while the program runs, and a Map also keeps number and object keys as they are.

The following example does the same tasks with both. It uses TypeScript 7.0.2 with strict on and Node.js 22.

// Record: a typed plain object
const rec: Record<string, number> = { Lokesh: 37 };
rec["Raj"] = 35;                                        // add
const r1 = rec["Raj"];                                  // r1 = 35
const r2 = Object.hasOwn(rec, "Raj");                   // r2 = true
delete rec["Raj"];                                      // remove
const rSize = Object.keys(rec).length;                  // rSize = 1
const rJson = JSON.stringify(rec);                      // rJson = {"Lokesh":37}

// Map: a collection class
const map = new Map<string, number>([["Lokesh", 37]]);
map.set("Raj", 35);                                     // add
const m1 = map.get("Raj");                              // m1 = 35
const m2 = map.has("Raj");                              // m2 = true
map.delete("Raj");                                      // remove
const mSize = map.size;                                 // mSize = 1
const mJson = JSON.stringify(Object.fromEntries(map));  // mJson = {"Lokesh":37}

// Convert one into the other
const toMap = new Map(Object.entries(rec));             // Map { "Lokesh" => 37 }
const toRecord = Object.fromEntries(map);               // { Lokesh: 37 }

Notice that the Record uses object syntax and Object functions, whereas the Map uses its own methods.

Next, we compare the TypeScript Map and the TypeScript Record in one table and go through the questions that decide the choice in a real project, such as the key type and the source of the keys. Finally, we convert one into the other.

1. A Type vs a Class

A Record does not exist at runtime. The compiler checks the annotation and removes it, so const rec: Record<string, number> = { Lokesh: 37 } becomes const rec = { Lokesh: 37 } in the JavaScript output. At runtime, rec is an ordinary object that inherits from Object.prototype. A Map is created with new Map() and stays a Map at runtime, which we can check with instanceof.

const rec: Record<string, number> = { Lokesh: 37 };
const map = new Map<string, number>([["Lokesh", 37]]);

const recIsMap = rec instanceof Map;                    // recIsMap = false
const mapIsMap = map instanceof Map;                    // mapIsMap = true
const recProto = Object.getPrototypeOf(rec) === Object.prototype;   // recProto = true

The type vs class difference explains most of the other differences, because a Record can do at runtime only what a plain object can do. The extra safety of a Record exists only in the compiler, and the extra behavior of a Map exists only at runtime.

2. Record vs Map Comparison Table

The table puts the runtime facts from MDN’s objects vs. maps comparison next to the compile-time facts from TypeScript. The first rows are runtime facts, and the later rows add the TypeScript-specific points.

PointRecord<K, V>Map<K, V>
What it isA compile-time type for a plain objectA runtime class
Key typesStrings and symbols; number keys are stored as stringsAny value: numbers, objects, functions
Order of entriesInteger-like keys sorted to the front, all other keys in insertion orderInsertion order for all keys
Counting entriesObject.keys(rec).lengthmap.size
Frequent adds and deletesNot optimized for it (MDN)Performs better (MDN)
JSONJSON.stringify() works directlyGives {}; convert with Object.fromEntries() first
Inherited keysInherits toString, constructor and others from Object.prototypeNone; only stored keys exist
Missing keys reported by the compilerYes, with a union key typeNo
Type of a missing-key readV (no undefined unless noUncheckedIndexedAccess is on)V | undefined
Spread, destructuring{ …rec }, const { Lokesh } = recNot supported; copy with new Map(map)
structuredClone()Copies itCopies it

Two rows correct claims that appear in many comparisons online.

  • The order of object keys is defined by the ECMAScript specification and is not random; the only unusual part is that integer-like keys such as “2” move to the front.
  • A Record does have iteration, through Object.entries(), Object.keys() and Object.values(); it only lacks methods on the object itself.

3. Questions That Decide Between Record and Map

The table lists facts, but a project needs a decision. Each question in this section points to one side, and in practice one or two of them settle the choice.

3.1. What Type Are the Keys?

Object keys can only be strings or symbols. A Record<number, V> compiles, but the keys come back as strings at runtime. A Map keeps every key as it is, including objects, which a plain object cannot use as keys at all.

// 1. Record: number keys become strings
const rec: Record<number, string> = { 1: "one" };
const recKeys = Object.keys(rec);                       // recKeys = ["1"]

// 2. Map: number keys stay numbers
const map = new Map<number, string>([[1, "one"]]);
const mapKeys = [...map.keys()];                        // mapKeys = [1]

// 3. Map: objects as keys
const lokesh = { name: "Lokesh" };
const visits = new Map<object, number>([[lokesh, 3]]);
const count = visits.get(lokesh);                       // count = 3

When keys are numbers that we compare or sort as numbers, or objects such as DOM nodes or class instances, we use a Map. A Map compares object keys by reference, so the same object must be used to read the value back.

3.2. Are the Keys Known When We Write the Code?

When the keys form a fixed list, a Record with a union key type is the stronger choice, because the compiler requires every key and rejects unknown ones. A Map<Fruit, number> restricts which keys may be stored, but it cannot require that all of them are present. For example, a shop page shows the stock for a fixed list of fruits, and with a Record, a forgotten fruit fails the build instead of showing an empty cell.

type Fruit = "apple" | "banana";

// 1. Record: the compiler requires every key
const stock: Record<Fruit, number> = { apple: 5, banana: 3 };
const apples = stock.apple;                             // type number, apples = 5

// 2. Map: any subset of keys compiles
const stockMap = new Map<Fruit, number>([["apple", 5]]);
const bananas = stockMap.get("banana");                 // type number | undefined, bananas = undefined

For open keys, the comparison turns around. A read from a Record<string, number> has the type number even when the key is missing, so the compiler does not force a check. Map.get() returns number | undefined, so the compiler makes us handle the missing case.

const rec: Record<string, number> = { Lokesh: 37 };
const map = new Map<string, number>([["Lokesh", 37]]);

const r = rec["Brian"];                                 // type number, value undefined
const m = map.get("Brian");                             // type number | undefined
const age = map.get("Brian") ?? 0;                      // age = 0

So the type checking favors a Record for fixed keys and a Map for open keys. The noUncheckedIndexedAccess compiler option closes the gap for a Record, at the cost of extra checks elsewhere in the code.

3.3. Do the Keys Come From User Input?

A plain object inherits properties from Object.prototype, so a lookup for a key that the object never stored can still find an inherited property, such as constructor. An inherited property breaks code that reads before it writes, and TypeScript cannot see the problem because the type says number.

const words = ["apple", "constructor", "apple"];

// 1. Record: "constructor" is found on Object.prototype
const counts: Record<string, number> = {};
for (const w of words) {
  counts[w] = (counts[w] ?? 0) + 1;
}
// counts = { apple: 2, constructor: "function Object() { [native code] }1" }

// 2. Map: only stored keys are found
const countMap = new Map<string, number>();
for (const w of words) {
  countMap.set(w, (countMap.get(w) ?? 0) + 1);
}
// countMap = Map { "apple" => 2, "constructor" => 1 }

// 3. Record without a prototype
const safe: Record<string, number> = Object.create(null);
for (const w of words) {
  safe[w] = (safe[w] ?? 0) + 1;
}
// safe = { apple: 2, constructor: 1 }

In the Record version, counts[“constructor”] returns the Object function, ?? keeps it because it is not null or undefined, and adding 1 turns it into a string. The program does not crash; it stores wrong data. A key named “__proto__” is worse, because assigning to it changes the object’s prototype instead of adding a key, which MDN lists as a source of prototype pollution attacks.

A Map has none of these problems, so we use it for keys that come from users or network requests. If a plain object is required, for example because a library expects one, Object.create(null) creates an object without a prototype, as in option 3.

3.4. How Is the Data Serialized or Copied?

Configuration and API payloads are often sent as JSON or copied with the spread syntax. A Record supports both without extra code, whereas a Map needs a conversion for JSON, because a Map stores its entries outside the object’s properties, where JSON.stringify() never looks.

const rec: Record<string, number> = { Lokesh: 37, Raj: 35 };
const map = new Map<string, number>([["Lokesh", 37], ["Raj", 35]]);

// 1. JSON
const j1 = JSON.stringify(rec);                         // j1 = {"Lokesh":37,"Raj":35}
const j2 = JSON.stringify(map);                         // j2 = {}

// 2. Copy with one change
const rec2 = { ...rec, Raj: 36 };                       // { Lokesh: 37, Raj: 36 }
const map2 = new Map(map).set("Raj", 36);               // Map { "Lokesh" => 37, "Raj" => 36 }

// 3. structuredClone() copies both
const recCopy = structuredClone(rec);                   // { Lokesh: 37, Raj: 35 }
const mapCopy = structuredClone(map);                   // Map { "Lokesh" => 37, "Raj" => 35 }

// 4. Destructuring works only on the object
const { Lokesh } = rec;                                 // Lokesh = 37

The empty {} from JSON.stringify(map) is a bug that shows no error and loses all data. For example, a shopping cart kept in a Map and saved with JSON.stringify() comes back empty after a page reload. For data that crosses a JSON boundary often, a Record avoids the conversion on every call. structuredClone() and APIs built on the same algorithm, such as postMessage() to a worker, copy a Map correctly, so the JSON limitation does not apply there.

3.5. How Often Do Entries Change?

MDN describes the Map as performing better “in scenarios involving frequent additions and removals of key-value pairs” and the plain object as “not optimized” for them. The exact difference depends on the engine and the data, so we treat it as a direction, not a number. One smaller point also favors a Map for changing data, because map.size is a property, while Object.keys(rec).length builds a new array of all keys every time we count.

For a lookup table that is built once and only read after that, such as labels or settings, the frequency of changes does not matter, and the other questions decide.

4. Converting Between Record and Map

A Map and an object can be converted into each other with two standard functions.

  • Object.entries() turns an object into [key, value] pairs, which the Map constructor accepts.
  • Object.fromEntries() builds an object from any iterable of pairs, including a Map.

A common design uses a Map inside the program and converts at the JSON boundary.

// 1. Record to Map and back
const rec: Record<string, number> = { apple: 5, banana: 3 };
const map = new Map(Object.entries(rec));               // Map<string, number>
const back = Object.fromEntries(map);                   // back = { apple: 5, banana: 3 }

// 2. Number keys do not survive the round trip
const squares = new Map<number, number>([[2, 4], [3, 9]]);
const obj = Object.fromEntries(squares);                // obj = { "2": 4, "3": 9 }
const again = new Map(Object.entries(obj));             // Map<string, number>
const nine = again.get("3");                            // nine = 9, the key is now "3"

The second example shows the trap. After the round trip, the keys are strings, so again.get(3) no longer finds the entry. TypeScript reports the again.get(3) call, because again has the type Map<string, number> and the argument 3 is not a string. For maps with number or object keys, we store […map], the array of [key, value] pairs, instead of an object.

5. Running the Comparison Code

The comparison project groups the snippets by section, and its entry file calls each group in turn. It builds with TypeScript 7.0.2 and needs at least Node.js 22.

npm install
npm start

The output of section 3.3 shows the broken constructor count from the Record next to the correct count from the Map, which is the quickest way to see why user-supplied keys belong in a Map.

6. Conclusion

A TypeScript Record is a compile-time type for a plain object, and a Map is a runtime collection. We choose a Record when the keys are strings known in advance, so the compiler can require every key of a union. A Record also fits data that moves through JSON or spread copies.

We choose a Map when keys are numbers or objects, or when they come from user input, where inherited object keys cause wrong results. A Map also fits entries that are added and removed while the program runs. Converting between the two takes one line, so a program can use a Map internally and a Record at its JSON edges.

7. References

MDN describes the runtime behavior of objects and maps, and the TypeScript Handbook documents the Record utility type.

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.