Kata: flatten with depth
Open arrays only, count depth down per level, write to a new list, and keep your own stack when nesting runs deep.
Predict first
Before you read: predict
ExamHall's question navigator numbers every question of an exam 1..N. Sections hold questions and groups of questions, and the navigator builds its list with one line:
What do list.length and list[2] hold?
const sections = [['q1', 'q2'], [['q3', 'q4'], 'q5'], 'q6'];export const list = [].concat(...sections);The 30-second version
flatten(arr, depth) returns a new array. It opens arrays only, while depth stays above 0, handing each nested list depth - 1; strings, objects and null stay whole, and empty slots are skipped, as flat() does. [].concat(...arr) opens one level. For deep nesting, keep your own stack of frames, not the call stack.
Open arrays only, count depth down per level, write to a new list, and keep your own stack when nesting runs deep.
How it works
Step 1 of 13
depth, everything else kept whole.Analogy: A county record office cataloguing a family deposit (heritage records)
A family deposits its papers at a county record office: boxes holding letters and folders, folders holding more letters. The archivist writes the catalogue, one numbered line per item.
| At the record office | In the kata |
|---|---|
| the catalogue is a new ledger; the deposit stays boxed as it came | a new out; the input is unchanged (step 7) |
| the family says how many levels to open: "boxes only", or "boxes and folders" | depth, counted down per level (step 4) |
| a letter gets one line, never one line per page | strings stay whole (step 3) |
| a pocket whose deed was withdrawn gets no line; an envelope marked "blank" gets one | holes skipped, undefined kept (step 8) |
| tipping each box onto the table once leaves the folders shut | [].concat(...arr) (step 2) |
| a slip on each open box: "next: item 4" | a frame { list, i, d } (step 12) |
| unopened boxes piled up come off the top first, so a pile alone writes the catalogue backwards | a plain stack reverses (step 11) |
In real terms: flattening is a walk over nested lists that appends each leaf to a new list in reading order, opening containers only to the given depth.
Breaks down when: an archivist sees a folder's contents at a glance; the code reads one slot at a time. And a deposit never contains itself, while an array can: ask about cycles (step 1).
Contrasting case: an archivist who tips every box onto the table once and numbers what falls out.
Examples, run for real
4.1 ExamHall EXH-3550: a one-level list, and the test list
ExamHall's navigator built its numbered list with [].concat(...sections). An exam with groups inside sections, and a group inside a group, got too few numbers (EXH-3550). MR !3850 replaced it with the reference answer and added the ten core tests plus the ladder test. The list also runs on two common answers: M1 is the one-line concat, M2 is the recursive walk (step 9).
const sections = [['Q1', 'Q2', ['Q3', 'Q4', 'Q5']], [['Q6', 'Q7'], 'Q8'], ['Q9', ['Q10', ['Q11', 'Q12']]], ['Q13']];
4.1 EXH-3550 [].concat(...sections): 8 numbers for 13 questions; 3 numbers land on a whole group
4.1 MR !3850 flatten(sections): 13 numbers for 13 questions, Q1..Q13 in reading order
4.1 reference.mjs 11 of 11 pass (10 core, 1 ladder)
4.1 M1, [].concat(...arr) 3 of 11 pass · red: T1, T4, T6, T7, T8, T9, T10, L1
4.1 M2, a recursive walk 10 of 11 pass · red: L1 (RangeError)
M1 ignores the depth and opens exactly one level, so it fails every test needing another number of levels, and the holes test: the spread turns an empty slot into undefined. M2 passes the core list and fails only L1, where each level costs a call frame (step 10).
4.2 Chat: a thread export, collapsed and expanded
A chat app exports a thread in which each reply's replies sit one level deeper. The collapsed view asks for depth 1, the full export for every level:
const thread = ['Ana: lunch?', ['Ben: yes', ['Ana: 12:30', ['Ben: ok']]], 'Cy: me too', ['Ana: great']];
4.2 flatten(thread, 1): 5 entries, 1 still a list (the deeper replies, collapsed)
4.2 flatten(thread): 6 entries, 0 lists
4.2 equal to thread.flat(1) and thread.flat(Infinity): true
4.2 thread unchanged after both calls: true
Depth 1 opens the thread's own lists once, so the reply chain below stays one list (step 4); with no depth, every level opens (step 6). Both match flat() given the same depth, and the thread is untouched (step 7).
Try it
Answer first, then check. Nothing is saved.
Question 1 of 8
ExamHall's navigator keeps its own stack of frames, one per open list. The loop's six lines are shuffled:
In what order does the loop run them, top first?
export function flatten(arr, depth = Infinity) { const out = []; const stack = [{ list: arr, i: 0, d: depth }]; while (stack.length) { /* the six lines, in order */ } return out;}Your order
- 1empty
- 2empty
- 3empty
- 4empty
- 5empty
- 6empty
Tap the lines in the order you expect.
Question 2 of 8
A video app flattens its nested playlists with a recursive kata answer that is complete apart from its condition:
The team's list has 10 tests; their items include titles, objects, null and undefined.
Which condition passes all 10?
export function flatten(arr, depth = Infinity) { const out = []; for (let i = 0; i < arr.length; i++) { if (!(i in arr)) continue; const v = arr[i]; if (/* the condition */) out.push(...flatten(v, depth - 1)); else out.push(v); } return out;}Question 3 of 8
A chat client shows a thread with replies nested under their message. Its kata answer uses a plain stack of values:
With thread = ['m1', ['r1', 'r2'], 'm2'], what does flatten(thread).join(' ') return?
export function flatten(arr) { const stack = [...arr]; const out = []; while (stack.length) { const v = stack.pop(); if (Array.isArray(v)) stack.push(...v); else out.push(v); } return out;}Question 4 of 8
A box office flattens its seat map with a recursive kata answer that is complete apart from the recursive call:
Which call passes all 10 tests of the list?
export function flatten(arr, depth = Infinity) { const out = []; walk(arr, depth, out); return out;} function walk(list, depth, out) { for (let i = 0; i < list.length; i++) { if (!(i in list)) continue; const v = list[i]; if (Array.isArray(v) && depth > 0) /* the call */; else out.push(v); }}Question 5 of 8
the interviewer says: "Implement flatten(arr, depth); with no depth, flatten every level. Don't change the input, and don't split strings." You have 15 minutes and a plain editor, and ExamHall's question navigator is the use case.
Before you type: say your test list out loud.
Name at least six tests, each with its input and what it expects.
Question 6 of 8
the interviewer says: "Implement flatten(arr, depth); with no depth, flatten every level. Don't change the input, and don't split strings." You have 15 minutes and a plain editor, and ExamHall's question navigator is the use case.
Now write it, in 15 minutes, from a blank file that exports flatten.
Question 7 of 8
A ledger export follows each posting with a list of its adjustments; a correction to an adjustment sits one list further down. A script flattens the lines by repeating one step until no list is left:
What is report?
let list = ['P-1', ['ADJ-1'], 'P-2', ['ADJ-2', ['ADJ-3']]];let passes = 0;while (list.some(Array.isArray)) { list = [].concat(...list); passes++;}export const report = `passes: ${passes} · entries: ${list.length}`;Question 8 of 8
A shop lists the products of a category tree with this kata answer:
For ['p1', ['p2', 'p3'], 'p4'] the page must show p1 p2 p3 p4, and it does not.
Which one change makes it show that?
export function flatten(arr) { const stack = [...arr]; const out = []; while (stack.length) { const v = stack.pop(); if (Array.isArray(v)) stack.push(...v.toReversed()); else out.push(v); } return out;}Go deeper
Pro tips
- When a runner is allowed, check your answer against
arr.flat(depth), and pass the depth. When: practising, or when the interviewer allows a runner (step 6). Test T7 usesflat()as an oracle over 20 cases;flat()defaults to 1, notInfinity. Verify (RAN,tips.mjs): (The curriculum row's tip, [SELF].)tip 1 T7, flat(depth) as the oracle over 20 cases: reference agrees · M1, [].concat(...arr) differs - Run your own answer against the list before you call it done:
KATA_IMPL=./my-flatten.mjs node --test kata-core.test.mjs, then the ladder. When: practising the kata at home (step 13). Verify (RAN,tips.mjs):tip 2 KATA_IMPL=./reference.mjs: exit 0 · tests 10 · pass 10 · fail 0 tip 2 KATA_IMPL=<temp>/m2-flatten.mjs (a recursive walk): core exit 0 · pass 10 of 10 · ladder exit 1 · red: L1
Source
Vendor and guide pages change, so their claims are labelled as of 2026-10-12; every behaviour this lesson grades is executed on Node 24.
- MDN, Array.prototype.flat() (the curriculum row's first source), https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat (
mdn-array-flat.txt).flat()"creates a new array with all sub-array elements concatenated into it recursively up to the specified depth"; its depth "Defaults to 1." (step 6); it "does not alter this but instead returns a shallow copy" (step 7); and "The flat() method removes empty slots in arrays" (step 8). As of 2026-10-12. - Front End Interview Handbook, JavaScript coding (the row's second source), https://www.frontendinterviewhandbook.com/coding/javascript-utility-function (
feih-javascript-coding.txt). Its question list includes "Flatten (Free)"; it says candidates take "around 10-15 minutes for a basic question" (the time box); it advises preparing by "writing test cases" (step 13), and to "clarify that there are no self-references/cycles within the object" (step 1). As of 2026-10-12.
Related lessons
Checked against the tools and docs as of 2026-10-12.