JavaScript Basics: Group Items with Object and Map
Grouping collects related items into arrays. Use a short label for each group, or use an object itself when its identity matters.
These examples use built-in features and were tested on Linux with Node.js v24.19.0. No packages are needed. Assertions check the results before the program finishes.
Prepare a scratch directory
You need Bash, a text editor, Node.js on your PATH, and the mktemp, rm, and rmdir utilities. Keep using the same terminal. These commands stop the shell if setup fails. Save each named source file as UTF-8; .mjs selects ES modules.
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 11. Group by a simple label
Save this as group-by-status.mjs:
import assert from 'node:assert/strict';
const tasks = [
{ title: 'Write outline', status: 'ready' },
{ title: 'Check links', status: 'waiting' },
{ title: 'Add examples', status: 'ready' },
];
const tasksByStatus = Object.groupBy(tasks, task => task.status);
const readyTitles = tasksByStatus.ready.map(task => task.title);
assert.deepEqual(readyTitles, ['Write outline', 'Add examples']);
assert.equal(Object.getPrototypeOf(tasksByStatus), null);
assert.equal(Object.hasOwn(tasksByStatus, 'waiting'), true);
console.log(`Ready: ${readyTitles.join(', ')}`);
console.log(`Null prototype: ${Object.getPrototypeOf(tasksByStatus) === null}`);
console.log(`Has waiting group: ${Object.hasOwn(tasksByStatus, 'waiting')}`);Run it:
node group-by-status.mjsOutput:
Ready: Write outline, Add examples
Null prototype: true
Has waiting group: trueObject.groupBy() calls the callback for each item and puts it in the matching array. It uses string or symbol property keys; other returned values are converted to property keys. Numbers such as 1 therefore become strings such as '1'.
The result has a null prototype, unlike an ordinary {} object. It does not inherit methods such as hasOwnProperty(), so use Object.hasOwn(result, key) to check for a group. Both grouping methods in this post are static: call Object.groupBy(items, callback) or Map.groupBy(items, callback), not items.groupBy().
2. Group by object identity
Save this as group-by-team.mjs:
import assert from 'node:assert/strict';
const morningTeam = { label: 'Editors' };
const eveningTeam = { label: 'Editors' };
const assignments = [
{ title: 'Review introduction', team: morningTeam },
{ title: 'Review conclusion', team: eveningTeam },
{ title: 'Review examples', team: morningTeam },
];
const assignmentsByTeam = Map.groupBy(assignments, item => item.team);
const morningTitles = assignmentsByTeam.get(morningTeam).map(item => item.title);
assert.equal(assignmentsByTeam.size, 2);
assert.equal(assignmentsByTeam.get(eveningTeam).length, 1);
assert.deepEqual(morningTitles, ['Review introduction', 'Review examples']);
assert.equal(assignmentsByTeam.has({ label: 'Editors' }), false);
console.log(`Team groups: ${assignmentsByTeam.size}`);
console.log(`Morning: ${morningTitles.join(', ')}`);
console.log(`New object matches: ${assignmentsByTeam.has({ label: 'Editors' })}`);Run it:
node group-by-team.mjsOutput:
Team groups: 2
Morning: Review introduction, Review examples
New object matches: falseMap.groupBy() accepts keys of any type. Two different objects remain different keys even when their properties match. Retrieve a group with the original key object. Changing that object’s properties does not change its identity.
For plain labels, Object.groupBy() is convenient. For object identity, use Map.groupBy(). Returning item.team.label instead would deliberately combine both teams under the same string label.
3. Understand shared items
Save this as group-shared-items.mjs:
import assert from 'node:assert/strict';
const tickets = [
{ title: 'Fix heading', status: 'open' },
{ title: 'Check footer', status: 'closed' },
];
const ticketsByStatus = Object.groupBy(tickets, ticket => ticket.status);
assert.equal(ticketsByStatus.open[0], tickets[0]);
ticketsByStatus.open[0].status = 'closed';
ticketsByStatus.open.push({ title: 'Add caption', status: 'open' });
assert.equal(tickets[0].status, 'closed');
assert.equal(tickets.length, 2);
assert.equal(ticketsByStatus.open.length, 2);
assert.equal(ticketsByStatus.closed.length, 1);
console.log(`Original first status: ${tickets[0].status}`);
console.log(`Original ticket count: ${tickets.length}`);
console.log(`Open group count: ${ticketsByStatus.open.length}`);
console.log(`Closed group count: ${ticketsByStatus.closed.length}`);Run it:
node group-shared-items.mjsOutput:
Original first status: closed
Original ticket count: 2
Open group count: 2
Closed group count: 1Both methods create new group arrays but keep references to the original objects; they do not clone the members. Updating a member is visible through the original array too. Pushing into a group array does not push into the input array. Group membership is decided when grouping runs: changing a ticket’s status does not move it automatically. Group again when you need fresh membership.
Clean up
In the same terminal, remove only the example files, return to your original directory, and remove the empty scratch directory:
rm -- group-by-status.mjs group-by-team.mjs group-shared-items.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirAcknowledgements
This post was created with assistance from AI.