Back to JavaScript
JavaScript
easy
junior

How would you flatten a deeply nested array of arbitrary depth without using 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.

4 min read·~12 min to think through

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).

ts
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).

ts
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

ts
function* flatGen(arr: any[]): Generator<any> {
  for (const v of arr) {
    if (Array.isArray(v)) yield* flatGen(v);
    else yield v;
  }
}
// Use: [...flatGen([1,[2,[3]]])] → [1,2,3]
// Lazy — useful when you only need the first N elements of a huge nested structure.
Generator-based lazy flatten

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.

Senior engineer discussion

Senior signal: choosing iterative vs recursive based on input characteristics, matching the spec's depth parameter, and avoiding the concat O(n²) pitfall.