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?
export function byBand(attempts, bandOf) { const bands = {}; attempts.forEach((a, i) => { (bands[bandOf(a, i)] ??= []).push(a.id); }); return bands;}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
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))?
export function tagCounts(sessions) { const counts = {}; for (const s of sessions) counts[s.tag] = (counts[s.tag] ?? 0) + 1; return counts;}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?
export function groupToMap(rows, keyOf) { const groups = new Map(); /* the six lines */ return groups;}Your order
- 1empty
- 2empty
- 3empty
- 4empty
- 5empty
- 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?
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;}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?
const shelfOf = (track, rank = 0) => track.genre ?? `chart ${rank + 1}`; export const shelves = Object.groupBy(tracks, shelfOf);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?
const bands = Object.groupBy(attempts, (a) => a.band);export const line = `bands: ${bands}`;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)?
const groups = Object.groupBy(sessions, (s) => s.tag);export const body = Object.assign({}, groups);Go deeper
Pro tips
- 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 - Check your test list against
Object.groupByitself: 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.
- MDN, Object.groupBy() (the curriculum row's first source), https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy (
mdn-object-groupby.txt): "The index of the current element being processed.", "A null-prototype object with properties for all groups, each assigned to an array containing the elements of the associated group." and "values that are neither type are coerced to strings" (step 2). As of 2026-10-16. - MDN, Map.groupBy() (the row's second source), https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/groupBy (
mdn-map-groupby.txt): "It should return a value (object or primitive) indicating the group of the current element." and "The returned result object is a Map so we need to call get() with the key to obtain the array." (step 10). As of 2026-10-16. - MDN, Object.prototype.__proto__, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto (
mdn-proto.txt): "The value provided must be an object or null." and "null-prototype objects don't inherit any property from Object.prototype, including the proto accessor property" (steps 5, 6, 8). As of 2026-10-16. - MDN, Object.assign(), https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign (
mdn-object-assign.txt): "It uses [[Get]] on the source and [[Set]] on the target, so it will invoke getters and setters." (step 11). As of 2026-10-16. - MDN, Set.prototype.forEach(), https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/forEach (
mdn-set-foreach.txt): "Key of each iteration. This is always the same as value." (step 9). As of 2026-10-16. - Front End Interview Handbook, JavaScript coding, https://www.frontendinterviewhandbook.com/coding/javascript-utility-function (
feih-javascript-coding.txt): candidates take "around 10-15 minutes for a basic question" (the time box), and it advises "writing test cases for them" (step 13). As of 2026-10-16.
Related lessons
Checked against the tools and docs as of 2026-10-16.