#Arrays#Polyfill

Array.prototype.flat Polyfill

Implement Array.prototype.flat from scratch with a configurable depth. Default depth is 1, not Infinity.

By Pratik RaiEasy

Array.prototype.flat returns a new array with nested sub-arrays merged into it, up to a given depth. It arrived late — ES2019 — and the reason for the delay is a genuinely interesting piece of web history. Implementing it means deciding between recursion, iteration, and a stack, and knowing when each one falls over.

Why implement it yourself

flat is a good exercise because the obvious recursive solution is elegant and has a real failure mode. It also has a default that surprises people, and a hole-removing behaviour that is not documented anywhere near prominently enough.

The default depth is 1

This is the most common flat mistake:

JSfile.javascript
1[1, [2, [3, [4]]]].flat(); // [1, 2, [3, [4]]] — only one level 2[1, [2, [3, [4]]]].flat(2); // [1, 2, 3, [4]] 3[1, [2, [3, [4]]]].flat(Infinity); // [1, 2, 3, 4]

flat() with no argument flattens exactly one level, not all of them. If you want a fully flat array, you must pass Infinity explicitly.

It also removes holes

A detail that is easy to miss and occasionally useful:

JSfile.javascript
1[1, , 3].flat(); // [1, 3] — the hole is gone

flat skips empty slots entirely, even at depth 0. That makes arr.flat(0) a concise way to densify a sparse array, though arr.filter(() => true) says it more clearly.

The recursive implementation

The natural version reads almost like the specification:

JSfile.javascript
1Array.prototype.myFlat = function (depth = 1) { 2 if (this === null || this === undefined) { 3 throw new TypeError('Array.prototype.myFlat called on null or undefined'); 4 } 5 6 const object = Object(this); 7 const length = object.length >>> 0; 8 const result = []; 9 10 for (let i = 0; i < length; i++) { 11 if (!(i in object)) continue; // skip holes 12 const value = object[i]; 13 if (depth > 0 && Array.isArray(value)) { 14 result.push(...value.myFlat(depth - 1)); 15 } else { 16 result.push(value); 17 } 18 } 19 20 return result; 21};

Array.isArray is the correct test rather than instanceof Array, which fails across realms — an array from an iframe or a Node vm context is a real array but not an instanceof match in the current realm.

Note this uses push(...), which reintroduces the argument-count limit from apply. On deeply nested data with very wide inner arrays, result.push(...huge) can throw a RangeError. Using a loop or result = result.concat(...) avoids it.

Why recursion is a liability

Every level of nesting adds a stack frame. Given a pathological input — a linked-list-shaped array nested tens of thousands deep — the recursive version blows the call stack:

JSfile.javascript
1let deep = [1]; 2for (let i = 0; i < 100000; i++) deep = [deep]; 3deep.flat(Infinity); // native handles it; naive recursion throws

The iterative version uses an explicit stack on the heap instead, which is bounded by memory rather than by stack depth:

JSfile.javascript
1function flatIterative(input, depth = 1) { 2 const stack = input.map((value) => [value, depth]); 3 const result = []; 4 5 while (stack.length) { 6 const [value, d] = stack.pop(); 7 if (d > 0 && Array.isArray(value)) { 8 for (let i = value.length - 1; i >= 0; i--) { 9 if (i in value) stack.push([value[i], d - 1]); 10 } 11 } else { 12 result.push(value); 13 } 14 } 15 16 return result; 17}

Pushing children in reverse and popping from the end preserves the original order while keeping the operation O(n).

For real-world data — JSON responses, component trees, nested menus — recursion is fine and reads far better. Reach for the iterative form when depth is genuinely unbounded or attacker-controlled.

flatMap is map followed by flat(1), in one pass:

JSfile.javascript
1[1, 2, 3].flatMap((n) => [n, n * 2]); // [1, 2, 2, 4, 3, 6]

Its most useful property is that returning [] drops an element, which makes it a combined map-and-filter:

JSfile.javascript
1users.flatMap((u) => (u.active ? [u.name] : []));

The depth is fixed at 1 and cannot be changed — flatMap(fn, 2) is not a thing.

Common follow-ups

"Flatten without recursion." The stack version above, or the loop-until-stable trick: while (arr.some(Array.isArray)) arr = [].concat(...arr); — concise, but quadratic in depth.

"What is the time complexity?" O(n) in the total number of elements across all levels, for both the recursive and iterative versions. The naive concat loop is O(n × depth) because it rebuilds the array each pass.

"Does it flatten array-likes?" No. Only genuine arrays are flattened; an object with a length is treated as a plain value.

Key takeaways

  • The default depth is 1, not Infinity.
  • flat removes holes at every depth, including 0.
  • Use Array.isArray, not instanceof, so it works across realms.
  • Recursion is clearer; an explicit stack survives unbounded nesting.
  • flatMap is map plus flat(1), and returning [] drops the element.

Goal: Implement myFlat with correct default depth and recursive depth tracking.

Frequently asked questions

What does Array.prototype.flat do, and what is the default depth?
It flattens nested arrays into a new array, one level deep by default. `flat(2)` goes two levels, and `flat(Infinity)` flattens completely regardless of nesting — which is why `Infinity` is worth mentioning even when the question does not ask for it.
What is the natural way to implement it?
Recursion with a decreasing depth counter: for each element, if it is an array and depth is still above zero, recurse with `depth - 1`; otherwise push it. `Array.isArray` is the correct test — `typeof` returns `"object"` for arrays and would let you flatten objects by mistake.
Does flat do anything besides flatten?
Yes, and it catches people out: `flat` removes holes in sparse arrays. `[1, , 3].flat()` gives `[1, 3]`, not `[1, undefined, 3]`. If your implementation copies every index blindly it will keep the holes and quietly disagree with the native method.
What are the follow-ups?
Writing it iteratively with a stack rather than recursively, which matters because deep nesting can exhaust the call stack. And implementing `flatMap`, which is `map` followed by a single level of flattening — the follow-up is usually whether you can explain why it only ever flattens one level.

Related Challenges

Continue learning with these related challenges

View All
JavaScript

Array.prototype.map Polyfill

Implement Array.prototype.map from scratch. Master callback signatures, thisArg binding, and sparse-array hole preservation.

JavaScript · Arrays · PolyfillsPratik Rai ·

JavaScript

Array.prototype.filter Polyfill

Implement Array.prototype.filter from scratch. The key detail: push the original element, not the boolean result of the predicate.

JavaScript · Arrays · PolyfillsPratik Rai ·

JavaScript

Array.prototype.reduce Polyfill

Implement Array.prototype.reduce from scratch, including the tricky no-initial-value path and the empty-array error case.

JavaScript · Arrays · PolyfillsPratik Rai ·