Lesson 35 of 80
Sorting items into buckets used to mean a reduce and a mutable accumulator. Now it's one call — as long as you know which of the two you actually want.
12 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
You have a flat array and you want it organized into buckets by some property — orders by status, users by role, log lines by severity. Object.groupBy and Map.groupBy both do exactly that: you hand them an array and a function that returns a "bucket name" for each item, and you get back a bucket-name-to-items-in-that-bucket structure. The only difference is what kind of structure you get back — a plain object, or a Map.
Before this, "group by" was a reduce you wrote from scratch every time, with a hasOwnProperty or ?? check to initialize each bucket the first time you saw it. It wasn't hard, but you wrote it constantly, and it was an easy place to get the initialization subtly wrong.
const orders = [
{ id: 1, status: "pending", total: 42 },
{ id: 2, status: "shipped", total: 15 },
{ id: 3, status: "pending", total: 30 },
{ id: 4, status: "delivered", total: 99 },
{ id: 5, status: "shipped", total: 8 },
];
const byStatus = Object.groupBy(orders, (order) => order.status);
// {
// pending: [{ id: 1, ... }, { id: 3, ... }],
// shipped: [{ id: 2, ... }, { id: 5, ... }],
// delivered: [{ id: 4, ... }],
// }
Compare that to the reduce version everyone has written a hundred times:
const byStatusOld = orders.reduce((acc, order) => {
(acc[order.status] ??= []).push(order);
return acc;
}, {});
Same result, but Object.groupBy reads as what it does, rather than how it does it.
Now the Map version — same idea, different container:
const byStatusMap = Map.groupBy(orders, (order) => order.status);
byStatusMap.get("pending"); // [{ id: 1, ... }, { id: 3, ... }]
Map.groupBy matters when your bucket key isn't a string. Say you want to group by the actual status object a lookup table returns, not its string name — plain objects can only have string or symbol keys, so Object.groupBy would coerce that key to "[object Object]" and silently merge every bucket into one. Map.groupBy accepts any value — an object, a number, NaN, even undefined — as a real, distinct key.
const byTotalBucket = Map.groupBy(orders, (order) =>
order.total >= 50 ? "big" : "small"
);
null prototype, not Object.prototype. Object.groupBy returns an object created with Object.create(null) specifically so a bucket literally named "toString" or "constructor" doesn't collide with an inherited method. It means byStatus.hasOwnProperty is undefined — use Object.hasOwn(byStatus, "pending") or "pending" in byStatus instead.Array.prototype methods. You call Object.groupBy(orders, fn), not orders.groupBy(fn). It's easy to reach for the latter out of muscle memory from .map/.filter.undefined, not an empty array. byStatus.cancelled is undefined if no order had that status — you still need byStatus.cancelled?.length or a fallback, exactly like the manual reduce version without the ??=.This is reduce with the accumulator-and-bucket bookkeeping built in — the same fold-into-an-accumulator shape as any reduce call, just named for the one job it's almost always used for. If you're comfortable with reduce, you already understand exactly what's happening under the hood here; Object.groupBy just means you stop writing that ??= line by hand.