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, so a Map<string, number> accepts only string keys and number values.

We use a Map for fast lookups by key, such as a shopping cart that stores the quantity for each product ID. A Map also fits when keys are not strings or when entries are added and removed while the program runs.

The Map collection is not the same thing as the array method map(), which transforms each element of an array and returns a new array. In this guide, we work only with the collection.

The following example shows every common Map operation in one snippet, written for TypeScript 7.0 in strict mode and Node.js 22.

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
const hasLokesh = ages.has("Lokesh");         // hasLokesh = true
const hasBrian = ages.has("Brian");           // hasBrian = 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

Notice that get() returns undefined for a missing key, so step 2 uses ?? 0 to fall back to a default value.

In the sections that follow, we look at each operation in detail and at the parts that cause bugs in real projects, such as get() returning undefined and a Map that turns into an empty JSON object. We also sort and group entries, and compare a Map with a plain object and a Record.

1. Creating a Map in TypeScript

We create a Map with the new keyword, and the two type parameters in angle brackets are the key type and the value type. In our examples, the Map stores people’s ages, with the name as the key and the age as the value.

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 the ages map in option 2 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");

An empty new Map() with no type parameters and no entries becomes Map<any, any>, so the compiler stops checking what goes into it. For an empty Map, we always write the types, as in new Map<string, number>().

2. Adding, Reading, Updating and Deleting Entries

A Map has a short list of methods, and each method does one job. The size member 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, because 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 is one of the most common uses of a Map, for example orders per customer or visits per page. We read the current count, fall back to 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. For example, a price lookup for a product code that a user typed into a search box returns undefined when the code does not exist. With strict mode turned on, using the result of get() 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 an error for 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 surprises many developers, because 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, because 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 we need to watch the parameter order.

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. A small cache uses the delete-and-add step on every read, so the most recently used key is always last and the first key is the one to evict.

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. The method 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. The method 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. JavaScript throws no error, 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().

The method Object.fromEntries() is safe only for string or number keys, because object keys all turn into the same string “[object Object]”. For a Map with object keys, 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. The parameter list ([a], [b]) takes the first element of each pair, which is the key. The list ([, 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 grouping. An order dashboard that shows one column per order status is a typical use. 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”) returns either an array of orders or undefined, and we use the optional chaining operator ?. before calling map() on it. A status with no orders, such as “cancelled”, does not appear in the result.

7. How a Map Compares Keys

A Map compares keys with the SameValueZero rule described in the ECMAScript specification. The SameValueZero rule works like the === operator, with the one exception that NaN matches NaN. The 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]]);

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

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

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 the runtime differences between objects and maps apply to a Record as well.

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() without conversion
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

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 change at runtime or are not strings, a Map is the better fit. For example, a chat server that keeps the user name for each open socket object uses a Map with the socket as the key.

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

When we need unique values without keys, the related collection is a TypeScript Set.

9. Running the Complete Example

The complete project on GitHub contains runnable versions of every snippet in this guide, 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"]
}

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 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.