Flatten a nested array of arbitrary depth (without Array.prototype.flat)
Recurse with reduce + concat for clarity, or iterate with a stack to avoid stack overflow on deep nesting. Support a depth parameter to match Array.prototype.flat semantics.
Flattening is a classic warm-up. The interviewer wants to see (a) a clean recursive solution, (b) awareness that recursion blows the stack on pathological input, (c) bonus for matching the spec's depth parameter.
Recursive (clean, idiomatic).
function flatten<T>(arr: any[], depth = Infinity): T[] {
return arr.reduce<T[]>((acc, val) => {
if (Array.isArray(val) && depth > 0) {
acc.push(...flatten<T>(val, depth - 1));
} else {
acc.push(val);
}
return acc;
}, []);
}
flatten([1, [2, [3, [4]]]]); // [1, 2, 3, 4]
flatten([1, [2, [3, [4]]]], 1); // [1, 2, [3, [4]]]Why a depth param. Matches [].flat(depth) — the standard library. Default Infinity flattens fully; 1 is the most common practical use.
Iterative (stack-safe for deep nesting).
function flattenIter<T>(arr: any[]): T[] {
const result: T[] = [];
const stack: any[] = [...arr];
while (stack.length) {
const next = stack.pop();
if (Array.isArray(next)) {
stack.push(...next);
} else {
result.push(next);
}
}
return result.reverse(); // because we used pop (LIFO)
}The recursive version blows the JS engine's call stack at ~10–15k levels of nesting. The iterative version uses heap memory (the stack array) instead and handles arbitrary depth.
Don't use concat recursion in a tight loop: acc = acc.concat(flatten(val)) allocates a new array every step — O(n²). Use push(...rest) or reduce with a single accumulator.
Watch out for sparse arrays. [1, , 3].flat() skips the hole. The above implementations don't, because Array.isArray is fine but iteration via reduce skips holes (matches the spec). Test with [1, , [2, , 3]] and confirm output is [1, 2, 3].
Common follow-up: deep flatten an object. Different problem entirely — use recursion with a path key ({a: {b: 1}} → {"a.b": 1}).
Performance. For huge arrays, prefer iterative with a pre-allocated result and push. For typical (<1k elements) cases, the recursive version is fine and readable.
Code
Follow-up questions
- •What's the time complexity? Space?
- •How would you flatten only one level (matching .flat(1))?
- •What about typed arrays or non-array iterables?
- •Implement a deep flatten for objects (path keys).
Common mistakes
- •Using `acc = acc.concat(...)` in recursion — O(n²) allocation.
- •Recursing without a depth bound — overflows on hand-crafted deep input.
- •Mutating the input array.
- •Treating strings as iterables — they're not arrays; don't flatten characters.
Performance considerations
- •Iterative + push beats recursive + concat for big inputs.
- •Generator version is best when consumers only read part of the result.
Edge cases
- •Empty array → []. Single element → [element].
- •Sparse arrays — holes are dropped in reduce-based versions (matches .flat).
- •depth = 0 → return a shallow copy (no flattening).
Real-world examples
- •Flattening route configs, tree-to-list serialization, normalizing nested API responses.