Skip to content

Kata: groupBy

Group into an object with no prototype; a {} already answers to names you never stored.

Predict first

Before you read: predict

ExamHall's results dashboard groups attempts into grade bands with this draft:

Band labels come from a settings screen that admins edit, and one admin saved the lowest band's label as '__proto__'. The dashboard runs byBand on three attempts whose bands are 'merit', 'pass' and '__proto__', in that order.

What does Object.keys(byBand(attempts, bandOf)) give?

Code for this question
export function byBand(attempts, bandOf) {  const bands = {};  attempts.forEach((a, i) => {    (bands[bandOf(a, i)] ??= []).push(a.id);  });  return bands;}
Pick one

The 30-second version

groupBy calls your key function with each item and its index, and files the item under the key it returns, keeping input order. A plain {} is not empty: a read of a missing name can find Object.prototype's methods or its __proto__ accessor. Start with no prototype, as Object.groupBy does, and every label is just a key.

Group into an object with no prototype; a {} already answers to names you never stored.

How it works

Step 1 of 13

Ask first. An object or a Map? Arrays only, or any iterable? Must every label work, __proto__ included?

Analogy: A spice merchant's second-hand drawer cabinet (trade history)

Each sack that reaches the shop is tipped into the drawer named on its label, in the order the sacks arrive.

At the cabinet In the kata
a drawer per label, filled in arrival order one group per key, input order kept (steps 2, 3)
the cabinet came with the last owner's drawers, "Ledger" and "Keys", already labelled a {} already has names (step 4)
one knob marked "Cabinet": pull it and out comes the cabinet's plan, not a drawer reading __proto__ (step 5)
a new cabinet, built with no drawers until a sack needs one no prototype (step 8)
a card box where each card keeps its label as written, word or number a Map (step 10)

In real terms: groupBy is one pass of reads and writes on a dictionary, and the dictionary must hold only the keys you put in it.

Breaks down when: a merchant reads each label before tipping and would query a sack marked "Cabinet"; code never looks at a key's spelling.

Contrasting case: labels the merchant writes herself, from a fixed list. The old cabinet is then safe enough; the hazard comes with labels that other people choose.

Examples, run for real

4.1 ExamHall EXH-4640: one band label stops the dashboard

The results dashboard grouped 2,400 attempts into bands with a draft that started from {} and created a band's list when !bands[label] was true. A tester saved the lowest band's label as constructor, to check that any label works, and the dashboard failed to load (EXH-4640). MR !4940 replaced the draft with Object.groupBy:

4.1 EXH-4640 the draft, 2,400 attempts, bands merit, pass and constructor: TypeError: bands[label].push is not a function
4.1 MR !4940 Object.groupBy, the same attempts: constructor 960 · merit 720 · pass 720 · the result's prototype: null

bands.constructor read the Object function from the prototype, so the draft skipped creating the list and pushed into a function (steps 4, 7). Object.groupBy's result has no prototype, so constructor is a band like the others (step 2).

4.2 Streaming: tags that viewers type

A video app shelves playlists by the tag each viewer typed, with Object.groupBy:

4.2 tags, in order: chill, __proto__, focus, __proto__, constructor
4.2 Object.groupBy: chill 1 · __proto__ 2 · focus 1 · constructor 1

Every tag, __proto__ included, is an ordinary group (steps 2, 8).

Try it

Answer first, then check. Nothing is saved.

Question 1 of 8

A video app counts viewing sessions per tag:

Four sessions are tagged 'live', '__proto__', 'live' and '__proto__', in that order.

What is JSON.stringify(tagCounts(sessions))?

Code for this question
export function tagCounts(sessions) {  const counts = {};  for (const s of sessions) counts[s.tag] = (counts[s.tag] ?? 0) + 1;  return counts;}
Pick one

Question 2 of 8

A video app needs Map.groupBy's result on an older runtime, so the team writes it by hand: one pass, keys kept as they are, the callback given each row and its position from 0. The skeleton:

Put the six lines in order, the first line first.

In what order do the lines run?

Code for this question
export function groupToMap(rows, keyOf) {  const groups = new Map();  /* the six lines */  return groups;}

Your order

  1. 1empty
  2. 2empty
  3. 3empty
  4. 4empty
  5. 5empty
  6. 6empty

Tap the lines in the order you expect.

Question 3 of 8

ExamHall's proctor view keeps each candidate's latest attempt, keyed by the username chosen at sign-up:

Two candidates signed up as constructor and __proto__. The view lists every other candidate, shows neither of them, and throws nothing.

Which one change makes it keep every candidate's latest attempt, theirs included?

Code for this question
export function latestByUser(attempts) {  const latest = {};  for (const a of attempts) {    if (!(a.user in latest) || a.at > latest[a.user].at) latest[a.user] = a;  }  return latest;}
Pick one

Question 4 of 8

A music app files tracks on genre shelves, reusing a helper written for the chart page, which labels a track with no genre by its chart place:

tracks is [{ title: 'Intro' }, { title: 'Blue', genre: 'jazz' }, { title: 'Outro' }, { title: 'Train', genre: 'jazz' }].

Which shelves does shelves hold, in any order?

Code for this question
const shelfOf = (track, rank = 0) => track.genre ?? `chart ${rank + 1}`; export const shelves = Object.groupBy(tracks, shelfOf);
Pick one

Question 5 of 8

the interviewer says: "Implement groupBy(items, keyFn) like Object.groupBy: any iterable, the callback gets each item and its index, and any key must work." You have 15 minutes and a plain editor, and a video app's watch history, grouped by the day each episode was watched, is the use case.

Before you type: say your test list out loud.

Name at least seven tests, each with its input and what it expects.

Question 6 of 8

the interviewer says: "Implement groupBy(items, keyFn) like Object.groupBy: any iterable, the callback gets each item and its index, and any key must work." You have 15 minutes and a plain editor, and a video app's watch history, grouped by the day each episode was watched, is the use case.

Now write it, in 15 minutes, from a blank file that exports groupBy.

Question 7 of 8

ExamHall's audit log writes one line per grading run:

attempts holds two attempts, in the bands 'merit' and 'pass'.

What happens when this module runs?

Code for this question
const bands = Object.groupBy(attempts, (a) => a.band);export const line = `bands: ${bands}`;
Pick one

Question 8 of 8

A video app's API copies the groups into a plain object before it sends them:

Three sessions are tagged 'live', '__proto__' and 'live', in that order.

What is Object.keys(body)?

Code for this question
const groups = Object.groupBy(sessions, (s) => s.tag);export const body = Object.assign({}, groups);
Pick one

Go deeper

Pro tips
  1. Run your own answer against the list before you call it done: KATA_IMPL=./my-group-by.mjs node --test kata.test.mjs. When: practising the kata at home, against the tests the reference passes (step 13). Verify (RAN, tips.mjs):
    tip 1 KATA_IMPL=./reference.mjs: exit 0 · tests 10 · pass 10 · fail 0
    tip 1 KATA_IMPL=<temp>/plain-group-by.mjs (a plain {} to start): exit 1 · tests 10 · pass 7 · fail 3 · red: T3, T4, T5
  2. Check your test list against Object.groupBy itself: Node 24's built-in passes all ten, so a test it fails is a wrong test. When: writing the list at home, and when the interviewer asks how close you are to the built-in (step 2). Verify (RAN, tips.mjs):
    tip 2 Node 24.21.0 Object.groupBy on the kata's list: 10 of 10 pass
Source

Vendor and guide pages change, so their claims are labelled as of 2026-10-16; every behaviour this lesson grades is executed on Node 24.

Checked against the tools and docs as of 2026-10-16.