TypeScript Map: Create, Iterate, Sort and Convert to JSON

TypeScript Map with tested examples: create, update and iterate entries, handle undefined from get(), convert to JSON, sort, and choose Map vs Record.

Decision diagram: Map for non-string keys or keys that change often at runtime, Record for fixed keys or JSON data

A TypeScript Map is a collection of key-value pairs where every key appears only once and the entries keep the order in which we added them. It is the standard JavaScript Map class with type parameters added: Map<string, number> tells the compiler that every key is a string and every value is a number. We use a Map when we need quick lookups by key, keys that are not strings, or a collection that grows and shrinks while the program runs.

This guide shows how to create a Map, add, read, update and delete entries, and loop over them. It also covers the parts that cause bugs in real projects: get() returning undefined, turning a Map into JSON, sorting, grouping with Map.groupBy(), and choosing between a Map, a plain object and a Record. Every snippet was compiled with TypeScript 7.0 in strict mode and run on Node.js 22.

The Map collection is not the same thing as the array method map(). The array method transforms each element of an array and returns a new array. This article is about the collection.

Every common Map operation fits in one snippet. The sections after it explain each step in detail.

const ages = new Map<string, number>();

// 1. Add entries
ages.set("Lokesh", 37);
ages.set("Raj", 35);
ages.set("John", 40);

// 2. Get an entry
const age = ages.get("John");                 // age = 40
const missing = ages.get("Brian") ?? 0;       // missing = 0

// 3. Check a key
ages.has("Lokesh");                           // true
ages.has("Brian");                            // false

// 4. Size of the Map
const count = ages.size;                      // count = 3

// 5. Update an entry
ages.set("Raj", 36);                          // replaces 35

// 6. Iterate in insertion order
for (const [name, value] of ages) {
  console.log(name, value);                   // Lokesh 37, Raj 36, John 40
}

// 7. Convert to JSON
const json = JSON.stringify(Object.fromEntries(ages));
// json = {"Lokesh":37,"Raj":36,"John":40}

// 8. Sort by value
const byAge = new Map([...ages].sort(([, a], [, b]) => a - b));
// byAge = Raj 36, Lokesh 37, John 40

// 9. Group a list (ES2024)
const groups = Map.groupBy([1, 2, 3, 4], (n) => (n % 2 === 0 ? "even" : "odd"));
// groups = odd => [1, 3], even => [2, 4]

// 10. Delete an entry
const isDeleted = ages.delete("Lokesh");      // isDeleted = true

// 11. Clear the whole Map
ages.clear();                                 // size = 0

1. Creating a Map in TypeScript

We create a Map with the new keyword. The two type parameters in angle brackets are the key type and the value type. In this guide, the Map stores people’s ages: the key is a name and the value is the age.

When we already know some entries, we pass them to the constructor as an array of [key, value] pairs. TypeScript reads those pairs and infers the types, so ages below becomes Map<string, number> without us writing the types. The set() method returns the Map itself, which lets us chain several calls.

// 1. Empty Map with explicit types
const empty = new Map<string, number>();

// 2. Initial entries; the type is inferred as Map<string, number>
const ages = new Map([
  ["Lokesh", 37],
  ["Raj", 35],
]);

// 3. Chained set() calls
const cities = new Map<string, string>()
  .set("Lokesh", "Delhi")
  .set("Raj", "Pune");

One detail matters for type safety. An empty new Map() with no type parameters and no entries becomes Map<any, any>, and the compiler stops checking what goes into it. For an empty Map, we always write the types: new Map<string, number>().

2. Adding, Reading, Updating and Deleting Entries

A Map has a short list of methods, and each one does exactly one job. size is a property, not a method, so we write ages.size without parentheses.

Method or propertyWhat it doesReturns
set(key, value)Adds an entry, or replaces the value when the key already existsThe Map itself
get(key)Reads the value stored for a keyThe value, or undefined if the key is missing
has(key)Checks whether a key existstrue or false
delete(key)Removes one entrytrue if the key existed, otherwise false
clear()Removes all entriesundefined
sizeCounts the entriesA number

There is no separate “update” method. Calling set() with a key that already exists replaces the old value.

const ages = new Map<string, number>();

// Add and update
ages.set("Lokesh", 37);
ages.set("Raj", 35);
ages.set("Raj", 36);                          // replaces 35

// Read
const age = ages.get("Lokesh");               // age = 37
const unknown = ages.get("Brian");            // unknown = undefined
const found = ages.has("Raj");                // found = true
const count = ages.size;                      // count = 2

// Delete
const deleted = ages.delete("Raj");           // deleted = true
const again = ages.delete("Raj");             // again = false

ages.clear();                                 // size = 0

Counting things is one of the most common uses of a Map: words in a text, orders per customer, visits per page. We read the current count, use 0 when the key is new, and write the result back. The ?? operator (nullish coalescing) returns the right side only when the left side is null or undefined.

const visits = new Map<string, number>();

visits.set("home", (visits.get("home") ?? 0) + 1);
visits.set("home", (visits.get("home") ?? 0) + 1);

const homeVisits = visits.get("home");        // homeVisits = 2

3. Handling undefined From get()

The return type of get() is the value type plus undefined. For Map<string, number>, it is number | undefined, because the compiler cannot know whether a key exists at runtime. With strict mode turned on, using that value as a plain number is a compile error.

const ages = new Map<string, number>([["Lokesh", 37]]);

const age: number = ages.get("Lokesh");

if (ages.has("Lokesh")) {
  const next = ages.get("Lokesh") + 1;
}

The TypeScript 7.0 compiler reports both lines:

src/err.ts(3,7): error TS2322: Type 'number | undefined' is not assignable to type 'number'.
  Type 'undefined' is not assignable to type 'number'.
src/err.ts(6,16): error TS2532: Object is possibly 'undefined'.

The second error is common. TypeScript does not track which keys a Map contains, so a successful has() check does not change the type of a later get() call. There are three clean ways to handle the missing case.

// 1. Default value
const age1 = ages.get("Brian") ?? 0;          // age1 = 0

// 2. Check the result; the type narrows to number
const age2 = ages.get("Lokesh");
if (age2 !== undefined) {
  console.log(age2 + 1);                      // 38
}

// 3. Non-null assertion after has()
if (ages.has("Lokesh")) {
  console.log(ages.get("Lokesh")! + 1);       // 38
}

Each option fits a different case:

  • Option 1 fits when a sensible default exists, such as 0 for a count.
  • Option 2 is the one we prefer in most code: it does one lookup instead of two and needs no assertion.
  • Option 3 works, but the ! operator turns off the undefined check for that expression, so the compiler will not warn us if a later change removes the key.

4. Iterating Over a Map

A Map always returns its entries in insertion order, the order in which the keys were first added. A for…of loop over the Map gives one [key, value] pair per step, and the brackets in const [name, age] split each pair into two variables (array destructuring). The keys() and values() methods return only one side, and the spread operator (…) turns them into arrays.

const ages = new Map([
  ["Lokesh", 37],
  ["Raj", 35],
  ["John", 40],
]);

// 1. Entries
for (const [name, age] of ages) {
  console.log(name, age);                     // Lokesh 37, Raj 35, John 40
}

// 2. Keys and values
const names = [...ages.keys()];               // names = ["Lokesh", "Raj", "John"]
const values = [...ages.values()];            // values = [37, 35, 40]

// 3. forEach: value first, then key
ages.forEach((age, name) => console.log(name, age));

The forEach() callback receives the value first and the key second, which is the opposite of the for…of pair. When the key and the value have the same type, swapping them does not cause a compile error, so the parameter order needs attention.

Updating a key and re-adding a key affect the order differently. Calling set() on an existing key keeps the key in its original position, while deleting a key and adding it again moves it to the end.

ages.set("Lokesh", 38);                       // stays first
ages.delete("Raj");
ages.set("Raj", 35);                          // moves to the end

const order = [...ages.keys()];               // order = ["Lokesh", "John", "Raj"]

5. Converting a Map to an Array, Object or JSON

Many APIs expect plain objects or arrays, and JSON has no Map type. Two standard functions handle most conversions: Object.fromEntries() builds an object from [key, value] pairs, and Object.entries() turns an object back into pairs that the Map constructor accepts.

const ages = new Map([["Lokesh", 37], ["Raj", 35]]);

// 1. Map to array and object
const pairs = [...ages];                      // pairs = [["Lokesh", 37], ["Raj", 35]]
const obj = Object.fromEntries(ages);         // obj = { Lokesh: 37, Raj: 35 }

// 2. Object to Map
const fromObj = new Map(Object.entries({ John: 40 }));   // John => 40

// 3. Map to JSON and back
const wrong = JSON.stringify(ages);           // wrong = {}
const json = JSON.stringify(Object.fromEntries(ages));   // json = {"Lokesh":37,"Raj":35}

const parsed: Record<string, number> = JSON.parse(json);
const restored = new Map(Object.entries(parsed));
const rajAge = restored.get("Raj");           // rajAge = 35

The JSON.stringify(ages) line is a common production bug. JSON.stringify() writes only an object’s own enumerable properties, and a Map keeps its entries in internal storage, so the result is an empty object. No error is thrown, so the API response or the saved file contains no data and nothing reports the problem. We always convert the Map before calling JSON.stringify().

Object.fromEntries() is safe only for string or number keys. If the Map uses objects as keys, every key turns into the same string “[object Object]”. For such a Map, we serialize the array of pairs from […map] instead.

6. Sorting a Map and Grouping With Map.groupBy()

A Map has no sort() method, because its order is always the insertion order. To sort it, we copy the entries into an array with […ages], sort the array, and build a new Map from the sorted pairs. The original Map stays unchanged.

const ages = new Map([["Raj", 35], ["John", 40], ["Lokesh", 37]]);

// 1. By key
const byName = new Map([...ages].sort(([a], [b]) => a.localeCompare(b)));
// byName = John 40, Lokesh 37, Raj 35

// 2. By value
const byAge = new Map([...ages].sort(([, a], [, b]) => a - b));
// byAge = Raj 35, Lokesh 37, John 40

The parameters of the compare function use destructuring again. ([a], [b]) takes the first element of each pair, which is the key. ([, a], [, b]) skips the key with an empty slot before the comma and takes the second element, which is the value.

The static method Map.groupBy(), added in ES2024, takes a list and a function that returns a group key for each item. It returns a Map from each group key to the items in that group. Before ES2024, we wrote a loop with get() and set() for this. To use it in TypeScript, the lib (or target) setting in tsconfig.json must include ES2024 or later; otherwise the compiler reports that groupBy does not exist on MapConstructor.

type Status = "new" | "shipped" | "cancelled";
interface Order { id: number; status: Status; }

const orders: Order[] = [
  { id: 1, status: "new" },
  { id: 2, status: "shipped" },
  { id: 3, status: "new" },
];

const byStatus = Map.groupBy(orders, (o) => o.status);   // Map<Status, Order[]>

const newIds = byStatus.get("new")?.map((o) => o.id);    // newIds = [1, 3]
const statuses = [...byStatus.keys()];                   // statuses = ["new", "shipped"]

TypeScript infers the result type Map<Status, Order[]> from the callback, so byStatus.get(“new”) is an array of orders or undefined. That is why the example uses the optional chaining operator ?. before calling map() on it. A status with no orders, such as “cancelled” here, does not appear in the result at all.

7. How a Map Compares Keys

A Map compares keys with the SameValueZero rule described in the ECMAScript specification. In practice, it works like the === operator, with one exception: NaN matches NaN. This rule leads to two behaviors that differ from plain objects.

// 1. The number 1 and the string "1" are different keys
const mixed = new Map<number | string, string>();
mixed.set(1, "number");
mixed.set("1", "string");
const size = mixed.size;                      // size = 2

// 2. A plain object turns both into the string "1"
const plain: Record<string, string> = {};
plain[1] = "number";
plain["1"] = "string";
const keys = Object.keys(plain).length;       // keys = 1

// 3. Object keys are compared by reference
interface User { id: number; name: string; }
const lokesh: User = { id: 1, name: "Lokesh" };
const visits = new Map<User, number>([[lokesh, 3]]);

visits.get(lokesh);                           // 3
visits.get({ id: 1, name: "Lokesh" });        // undefined

A plain object converts both keys to the string “1”, so the second assignment overwrites the first. Object keys are compared by reference: a new object with the same fields is a different key, so get() returns undefined. When the “same” object can be re-created (for example, loaded again from a database), a stable ID such as lokesh.id is the safer key.

8. Map vs Object vs Record in TypeScript

TypeScript gives us three ways to store key-value data:

  • a Map
  • a plain object
  • the Record<K, V> type, which describes an object whose keys are of type K and values of type V

A Record is still a plain object at runtime, so it behaves like an object in the comparison below. The MDN comparison of objects and maps is the source for the runtime rows.

NeedMapObject or Record
Key typesAny value: string, number, object, functionStrings and symbols; numbers become strings
Order of entriesAlways insertion orderInteger-like keys come first in ascending order, then other keys in insertion order
Number of entriesmap.sizeObject.keys(obj).length
Frequent adds and deletesDesigned for itNot optimized for it
JSONNeeds conversion firstWorks with JSON.stringify() directly
Compiler checks a fixed set of keysNoYes, for example Record<“NEW” | “SHIPPED”, number> must have both keys
Built-in keys from the prototypeNoneInherits keys such as constructor and toString

A short rule covers most cases. When the keys are known in advance and the data goes to or comes from JSON (configuration, API payloads), a Record or an interface is the better fit. When keys are added and removed at runtime, are not strings, or come from user input, a Map is the better fit.

Decision diagram: Map for non-string keys or keys that change often at runtime, Record for fixed keys or JSON data
Choose by the keys: a fixed set of keys or JSON data points to a Record; keys that change at runtime or are not strings point to a Map.

The article TypeScript Record vs Map compares the two in more detail, and TypeScript Set covers the related collection that stores unique values without keys.

9. Running the Complete Example

The complete project on GitHub contains runnable versions of every snippet above, an index.ts that runs them in order, and tests for the helpers written with the built-in Node.js test runner. It uses TypeScript 7.0.2 and needs Node.js 22 or newer.

The tsconfig.json sets the ES2024 library for Map.groupBy(), uses Node.js-style ES modules, and turns on strict mode, which gives us the undefined checks from section 3.

{
  "compilerOptions": {
    "target": "es2024",
    "lib": ["es2024"],
    "module": "nodenext",
    "strict": true,
    "rootDir": ".",
    "outDir": "dist",
    "types": ["node"],
    "skipLibCheck": true
  },
  "include": ["src", "test"]
}

After cloning the repository, we run these commands inside the typescript-maps folder:

npm install
npm start
npm test

The npm start script compiles the project and prints the values shown in the comments of each snippet. The npm test script compiles it again and runs five tests that cover counting, conversion to and from objects, sorting, grouping, and the empty {} result of JSON.stringify().

10. Conclusion

A TypeScript Map stores key-value pairs with typed keys and values, keeps insertion order, and gives us set(), get(), has(), delete() and clear() for everyday work. The parts that need extra care are get(), which can return undefined, and JSON.stringify(), which writes an empty object unless we convert the Map first. For data with a fixed set of string keys that is sent or stored as JSON, a Record is usually simpler. For data that changes at runtime, a Map is the better tool.

11. References

MDN and the ECMAScript specification describe how a Map behaves at runtime, and the TypeScript Handbook explains the generic type parameters used in every example.

Happy Learning !!

Source Code on Github

Leave a Comment

  1. I have a map that consists of key, value pairs where value is in the form of an array of objects i.e. basically of the form
    myMap
    a, 123
    b , 45
    c, 6789
    I tried printing it on the console with the help of nested for loops but it just won’t run.
    Could you help me out ??

Comments are closed.

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.