Skip to content
aviral gupta

// I5.1 · ~34 min · Intermediate

Map and Set: data by key, values once

After this lesson you store and look up data by key with a Map, remove duplicates with a Set, and count or group a list in a few lines.

Lesson 1 of 5 in I5 Working with data

Start of the module

You will be able to

  • Store, read and loop over data by key with Map: set, get, has, delete and size
  • Keep unique values with Set, and convert between arrays, objects, Maps and Sets
  • Count and group values with a Map, with the has/set pattern or ES2026 getOrInsert
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson B5.3. What does this print?

    const prices = { tea: 3, cake: 4 };
    console.log(Object.entries(prices));
  2. Predict · Activity 2 of 7

    Predict before you read on. The same two keys go into an object and into a Map. What is printed, read as one line?

    const seen = {};
    seen[1] = "number one";
    seen["1"] = "string one";
    console.log(Object.keys(seen).length);
    
    const map = new Map();
    map.set(1, "number one");
    map.set("1", "string one");
    console.log(map.size);
  3. Practice · Activity 3 of 7

    Fill in the collection that removes the duplicates.

    const unique = [...new ____(["de", "en", "de", "fr"])];
    const unique = [...new(["de", "en", "de", "fr"])];
  4. Practice · Activity 4 of 7

    Match each Map member to what it gives you.

  5. Practice · Activity 5 of 7

    A Map counts the words. What does this print?

    const words = ["a", "b", "a", "c", "a"];
    const counts = new Map();
    for (const word of words) {
      counts.set(word, (counts.get(word) ?? 0) + 1);
    }
    console.log([...counts]);
  6. Brain teaser · Activity 6 of 7

    Brain teaser. The setting is stored with brackets. What does this print?

    const settings = new Map();
    settings["theme"] = "dark";
    console.log(settings.size, settings.has("theme"));
  7. Apply · Activity 7 of 7

    Mini-task: you have words = ["apple", "bread", "avocado", "butter", "cheese"]. Group them by their first letter in a Map, using the has/set pattern. Then loop over the Map and print one line per letter, such as a: apple, avocado, and finally the number of letters.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

Visitors, page views and visitors per page

A list of website visits is turned into three answers. A Set gives the visitors, each once. A Map counts the views per page. A second Map groups the visitors by page: pushTo() uses ES2026 getOrInsert where the runtime has it and the classic has/set pattern elsewhere, so it prints the same in every runtime. An object key shows that a Map finds keys by identity, and Object.fromEntries prepares the counts for JSON.

main.js

// Visits to a small website: [visitor, page].
const visits = [
  ["ada", "/home"], ["bo", "/blog"], ["ada", "/blog"],
  ["cy", "/home"], ["bo", "/blog"], ["ada", "/home"],
];

// 1. A Set keeps each value once, in the order it first came.
const visitors = new Set(visits.map(([visitor]) => visitor));
console.log("visitors:", visitors.size, [...visitors].join(", "));

// 2. Counting: one Map entry per page.
const views = new Map();
for (const [, page] of visits) {
  views.set(page, (views.get(page) ?? 0) + 1);
}
for (const [page, count] of views) console.log(page, count);

// 3. Grouping: page -> array of visitors.
function pushTo(map, key, value) {
  if (typeof Map.prototype.getOrInsert === "function") {
    map.getOrInsert(key, []).push(value); // ES2026 shortcut
  } else {
    if (!map.has(key)) map.set(key, []); // the classic way
    map.get(key).push(value);
  }
}
const byPage = new Map();
for (const [visitor, page] of visits) pushTo(byPage, page, visitor);
console.log("/blog:", byPage.get("/blog").join(" "));

// 4. Any value can be a key, even an object.
const ada = { name: "Ada" };
const lastSeen = new Map([[ada, "/home"]]);
console.log(lastSeen.get(ada), lastSeen.get({ name: "Ada" }));

// 5. Back to a plain object, for JSON.
console.log(JSON.stringify(Object.fromEntries(views)));

Run it with

node main.js

Output

visitors: 3 ada, bo, cy
/home 3
/blog 3
/blog: bo ada bo
/home undefined
{"/home":3,"/blog":3}
  • The Set kept each visitor once, in the order of the first visit.
  • The Map listed /home before /blog because /home was counted first.
  • lastSeen.get({ name: "Ada" }) is undefined: an equal-looking object is still a different key.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Exercises

Exercise 1 of 2

Count the words

countWords(text) should return a Map from each word to how often it occurs, ignoring case: countWords("The cat and the hat").get("the") is 2. Words are separated by single spaces. The starter sets every count to 1. Fix it so that a word seen again adds 1 to its count.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    Run the tests: the is counted once. What does the starter do when it meets a word for the second time?

  2. Hint 2

    Read the old count first. counts.get(word) is undefined for a new word.

  3. Hint 3

    counts.set(word, (counts.get(word) ?? 0) + 1);

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export function countWords(text) {
  const counts = new Map();
  for (const word of text.toLowerCase().split(" ")) {
    counts.set(word, (counts.get(word) ?? 0) + 1);
  }
  return counts;
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

export function countWords(text) {
  const counts = new Map();
  for (const word of text.toLowerCase().split(" ")) {
    counts.set(word, 1);
  }
  return counts;
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {countWords} from './main.js';

test('countWords returns a Map', () => {
  const counts = countWords('a b');
  assert.ok(counts instanceof Map, `countWords returned ${counts}, not a Map`);
});

test('A repeated word is counted each time', () => {
  const counts = countWords('the cat and the hat');
  assert.equal(counts.get('the'), 2, `the was counted ${counts.get('the')} times`);
  assert.equal(counts.get('cat'), 1, `cat was counted ${counts.get('cat')} times`);
});

test('Case is ignored and the order is first seen', () => {
  const counts = countWords('Go go GO stop');
  assert.deepEqual([...counts], [['go', 3], ['stop', 1]], `the entries are ${JSON.stringify([...counts])}`);
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Exercise 2 of 2

Group orders by customer

groupOrders(orders) gets objects like {id: 7, customer: "ada"} and should return a Map from each customer to the array of their order ids, in order. The starter makes a new array for every order, so each customer keeps only the last id. Create the array only for a new customer, then push. Use has and set; do not rely on getOrInsert.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the JavaScript runner (up to 0.1 MB) and keeps it cached. Your code runs in your browser’s own engine and stays on your device.

Hints
  1. Hint 1

    Run the tests: ada has only [4]. When does byCustomer.set(order.customer, []) run?

  2. Hint 2

    Make the empty array only when the customer is not in the Map yet: byCustomer.has(order.customer).

  3. Hint 3

    if (!byCustomer.has(order.customer)) byCustomer.set(order.customer, []);

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export function groupOrders(orders) {
  const byCustomer = new Map();
  for (const order of orders) {
    if (!byCustomer.has(order.customer)) byCustomer.set(order.customer, []);
    byCustomer.get(order.customer).push(order.id);
  }
  return byCustomer;
}
Run it on your computer

Install ECMAScript 2026 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.js

export function groupOrders(orders) {
  const byCustomer = new Map();
  for (const order of orders) {
    byCustomer.set(order.customer, []);
    byCustomer.get(order.customer).push(order.id);
  }
  return byCustomer;
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {groupOrders} from './main.js';

const orders = [
  {id: 1, customer: 'ada'},
  {id: 2, customer: 'bo'},
  {id: 3, customer: 'ada'},
  {id: 4, customer: 'ada'}
];

test('Each customer keeps all their order ids', () => {
  const groups = groupOrders(orders);
  assert.deepEqual(groups.get('ada'), [1, 3, 4], `ada has ${JSON.stringify(groups.get('ada'))}`);
  assert.deepEqual(groups.get('bo'), [2], `bo has ${JSON.stringify(groups.get('bo'))}`);
});

test('Customers come in the order of their first order', () => {
  const groups = groupOrders(orders);
  assert.deepEqual([...groups.keys()], ['ada', 'bo'], `the keys are ${JSON.stringify([...groups.keys()])}`);
});

test('No orders give an empty Map', () => {
  const groups = groupOrders([]);
  assert.equal(groups.size, 0, `the Map has ${groups.size} entries`);
});

package.json

{
  "type": "module"
}

package.json tells Node.js that the .js files are modules; keep it in the folder.

Run the program:

node main.js

Run the checks (needs learnrun.js in the same folder):

node --test
Download learnrun.js

Common mistakes

Storing with brackets instead of set

const prices = new Map();
prices["tea"] = 3;
console.log(prices.get("tea").toFixed(2));

What Node.js prints

TypeError: Cannot read properties of undefined (reading 'toFixed')

Why, and the fix

prices["tea"] = 3 adds an ordinary property to the Map object. The Map’s entries stay empty, so prices.get("tea") is undefined. Write with prices.set("tea", 3) and read with prices.get("tea"). The same goes for has, delete and size: they only see entries made with set.

Passing an object to new Map

const prices = new Map({ tea: 3, cake: 4 });
console.log(prices.size);

What Node.js prints

TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))

Why, and the fix

new Map wants a list of [key, value] pairs, not a plain object. Turn the object into pairs first: new Map(Object.entries({ tea: 3, cake: 4 })). The way back is Object.fromEntries(map).

Pushing before the group exists

const byPage = new Map();
for (const [visitor, page] of [["ada", "/home"], ["bo", "/blog"]]) {
  byPage.get(page).push(visitor);
}

What Node.js prints

TypeError: Cannot read properties of undefined (reading 'push')

Why, and the fix

The first time a page comes up, the Map has no array for it, so get returns undefined and push fails. Create the array for a new key first: if (!byPage.has(page)) byPage.set(page, []); then byPage.get(page).push(visitor). Where ES2026 getOrInsert exists, byPage.getOrInsert(page, []).push(visitor) does both.

JavaScript in the browser: your browser’s own engine, in a sandboxed worker. Syntax errors are located with acorn 8.18.0, MIT. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Map: any key, kept in order

A Map holds key-value pairs. map.set(key, value) adds or replaces, map.get(key) reads (undefined when missing), map.has(key) asks, map.delete(key) removes and map.size counts. Unlike an object, whose keys are always strings or symbols, a Map keeps any value as a key: the number 1 and the string "1" are two keys, and an object key is found only by the same object, not by an equal-looking one. A for...of loop over a Map gives [key, value] pairs in the order the keys were first set. Use set and get, not map[key]: brackets make an ordinary property that the Map does not see.

Set, and converting

A Set keeps each value once: add(value), has(value), delete(value) and size. new Set(array) removes duplicates, and [...set] or Array.from(set) turns it back into an array in first-seen order. Like Map keys, objects in a Set are compared by identity, so two separate [1, 2] arrays are two values. To convert, new Map(Object.entries(object)) turns an object into a Map, Object.fromEntries(map) turns a Map back into an object (for JSON.stringify, which prints a Map as {}), and [...map] gives an array of [key, value] pairs.

Counting and grouping

To count, read the old count with a default and write it back: counts.set(word, (counts.get(word) ?? 0) + 1). To group, make sure an array exists first, then push: if (!map.has(key)) map.set(key, []); map.get(key).push(item). Map.groupBy(items, fn) groups a whole array in one call. ECMAScript 2026 adds map.getOrInsert(key, defaultValue), which returns the existing value or inserts the default: map.getOrInsert(key, []).push(item). Node.js 24 does not have it yet, so this course grades the has/set pattern. Your browser may already have it; check with typeof Map.prototype.getOrInsert === "function".

Sources

Last reviewed October 5, 2026