Skip to content
JavaScript Basics: Count, Look Up, and Deduplicate with Map and Set

JavaScript Basics: Count, Look Up, and Deduplicate with Map and Set

October 14, 2026

An array is convenient when order and repeated values matter. Sometimes your real question is different: how many times did this label appear, which record has this ID, or have I already seen this value? JavaScript’s Map and Set make those jobs direct.

A Map associates keys with values. A Set stores unique values. We will use both in three independent examples, then examine why two objects with identical fields still count as different values.

Prepare a scratch directory

These examples were tested on Linux with Node.js v24.19.0. Prerequisites are Node.js on your PATH, Bash, the standard mktemp, rm, and rmdir commands, and a text editor. No external dependencies, network access, environment-variable configuration, or input files are required. Files use .mjs so Node recognizes JavaScript modules without a package file. The small checks use the built-in node:assert/strict module.

Run this in Bash and leave the same shell open until cleanup:

node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

The tested version command printed:

v24.19.0

Save each following JavaScript block under its named filename in this directory, using UTF-8 encoding in your editor. Run each file with its separate Bash command.

Count labels with a Map

Save as count-labels.mjs:

import assert from 'node:assert/strict';

function countLabels(labels) {
  const counts = new Map();
  for (const label of labels) {
    counts.set(label, (counts.get(label) ?? 0) + 1);
  }
  return counts;
}

const counts = countLabels(['tea', 'coffee', 'tea', 'water', 'coffee']);
assert.equal(counts.get('tea'), 2);
assert.equal(counts.has('juice'), false);
assert.equal(countLabels([]).size, 0);
assert.equal(countLabels(['__proto__', '__proto__']).get('__proto__'), 2);
for (const [label, count] of counts) {
  console.log(`${label}: ${count}`);
}
node count-labels.mjs

Output:

tea: 2
coffee: 2
water: 1

get() returns undefined for a missing key. The ?? 0 expression supplies the initial count, and set() stores the updated number. Iteration follows the order in which keys first entered the Map; updating a count does not move its key. Map entries also avoid collisions with object-property names such as __proto__. These operations are defined in the ECMAScript Map specification.

Use .size to count entries, not .length. Use set() and get(), not bracket assignment, to work with Map entries.

Build a lookup and choose a duplicate policy

Save as lookup-products.mjs:

import assert from 'node:assert/strict';

function indexProducts(products) {
  const byId = new Map();
  for (const product of products) {
    if (byId.has(product.id)) {
      throw new Error(`Duplicate product ID: ${product.id}`);
    }
    byId.set(product.id, product);
  }
  return byId;
}

const products = indexProducts([
  { id: 'p1', name: 'Notebook' },
  { id: 'p2', name: 'Pencil' },
]);
assert.equal(products.get('p2').name, 'Pencil');
assert.equal(products.get('missing'), undefined);
assert.throws(() => indexProducts([{ id: 'p1' }, { id: 'p1' }]),
  /Duplicate product ID/);
const optional = new Map([['pending', undefined]]);
assert.equal(optional.has('pending'), true);
assert.equal(optional.has('missing'), false);
console.log(`Found: ${products.get('p2').name}`);
console.log(`Missing: ${products.has('p3')}`);
node lookup-products.mjs

Output:

Found: Pencil
Missing: false

These local records deliberately use string IDs. A plain set() would replace an existing value for the same key. Here, duplicates indicate bad input, so the function rejects them before overwriting anything. Other applications might intentionally keep the first or last record; choose that policy explicitly.

Use has() when stored values can themselves be undefined. A get() result alone cannot distinguish an absent entry from an entry containing undefined. Maps also distinguish numeric 1 from string '1', so normalize external IDs if your application treats those forms as equivalent.

Deduplicate with a Set, respecting identity

Save as deduplicate-values.mjs:

import assert from 'node:assert/strict';

const labels = [...new Set(['red', 'blue', 'red', 'green'])];
assert.deepEqual(labels, ['red', 'blue', 'green']);
assert.equal(new Set([]).size, 0);
assert.equal(new Set([NaN, NaN, 0, -0]).size, 2);

const first = { id: 'p1' };
const second = { id: 'p1' };
const objects = new Set([first, first, second]);
assert.equal(objects.size, 2);
assert.equal(objects.has({ id: 'p1' }), false);

const ids = new Set([first, second].map(product => product.id));
assert.equal(ids.size, 1);
const owners = new Map([[first, 'A']]);
assert.equal(owners.get(second), undefined);
console.log(`Labels: ${labels.join(', ')}`);
console.log(`Objects: ${objects.size}; IDs: ${ids.size}`);
node deduplicate-values.mjs

Output:

Labels: red, blue, green
Objects: 2; IDs: 1

Spreading a Set into an array preserves first-insertion order. Repeating the same object reference adds nothing, but a separately created object is distinct even when its fields match. Object keys in Maps follow the same identity rule. To deduplicate records by ID, compare their IDs, not the record objects themselves. The ECMAScript Set specification also defines the equality behavior illustrated by repeated NaN and signed zero.

Clean up

Remove only these named files, return to your starting directory, and remove the now-empty scratch directory:

rm -- count-labels.mjs lookup-products.mjs deduplicate-values.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

Choose a Map when each key needs an associated value. Choose a Set when membership is the answer. In both cases, define what makes two items “the same” before building your collection.

Acknowledgements

This post was created with assistance from AI.

Last updated on