Skip to main content

Flattening Arrays in JavaScript: A Complete Guide with Interview Scenarios

Updated 13 min readMarkdown
Flattening Arrays in JavaScript: A Complete Guide with Interview Scenarios

Arrays are one of the most fundamental data structures in JavaScript. You use them constantly — for lists of users, collections of IDs, API responses, and more. But real-world data is rarely flat. You often end up with arrays that contain other arrays, which contain more arrays, nested several levels deep. Before you can work with that data meaningfully, you need to flatten it.

This guide walks through what nested arrays are, why flattening matters, and every practical technique you should know — including the ones that show up in interviews.


What Is a Nested Array?

A nested array is simply an array that contains one or more arrays as its elements. Those inner arrays can themselves contain arrays, and so on.

// One level deep
const oneLevel = [1, [2, 3], [4, 5]];

// Two levels deep
const twoLevels = [1, [2, [3, 4]], [5, [6, 7]]];

// Arbitrarily deep
const deep = [1, [2, [3, [4, [5]]]]];

Visually, think of it like boxes inside boxes:

outerArray
└── [ 1, innerArray, innerArray ]
         └── [ 2, deepArray ]
                   └── [ 3, 4 ]   ← leaf values

Each level adds a layer of nesting. The actual values — numbers, strings, objects — live at the innermost level. To get at them without worrying about the structure, you need to flatten.


Why Does Flattening Matter?

Nested arrays come up naturally whenever data is hierarchical or composed from multiple sources. Here are the situations where you will encounter them most often:

API responses. A response might return paginated chunks, each being an array. You fetch three pages and end up with [[...], [...], [...]] — three arrays you need to merge into one.

Tree-shaped data. Categories with subcategories, comments with replies, folder structures — all of these are naturally recursive and often arrive pre-nested.

Array.map() producing arrays. If you map over a list and each callback returns an array, you get an array of arrays instead of a single flat result. This is the most common accidental nesting in everyday JavaScript.

const sentences = ["hello world", "foo bar"];
const words = sentences.map(s => s.split(" "));
// [["hello", "world"], ["foo", "bar"]]
// You wanted: ["hello", "world", "foo", "bar"]

Set operations. Grouping, partitioning, or combining arrays by some criteria produces nested structures you later need to linearize.

In all these cases, working with a single, flat array is far simpler — easier to iterate, filter, search, and pass around.


The Concept of Flattening

Flattening means taking a nested array and producing a new array that contains all the same values, but without any nesting. Every inner array is "unwrapped" so its elements become direct children of the result.

Input:   [1, [2, 3], [4, [5, 6]]]
                              ↓  flatten
Output:  [1, 2, 3, 4, 5, 6]

The key decision when flattening is depth — how many levels of nesting to remove:

  • Depth 1: unwrap only the outermost arrays. Inner arrays that are nested deeper remain.

  • Depth Infinity: unwrap everything, no matter how deep.

  • Specific depth (2, 3, ...): unwrap exactly that many levels.

const arr = [1, [2, [3, [4]]]];

arr.flat(1);        // [1, 2, [3, [4]]]   — one level removed
arr.flat(2);        // [1, 2, 3, [4]]     — two levels removed
arr.flat(Infinity); // [1, 2, 3, 4]       — fully flat

With that mental model in place, let's look at every approach available.


Approach 1: Array.flat() — the modern standard

Introduced in ES2019, Array.flat() is the cleanest and most readable way to flatten arrays. It takes an optional depth argument (default is 1) and returns a new flattened array without mutating the original.

const arr = [1, [2, 3], [4, [5, 6]]];

arr.flat();         // [1, 2, 3, 4, [5, 6]]
arr.flat(2);        // [1, 2, 3, 4, 5, 6]
arr.flat(Infinity); // [1, 2, 3, 4, 5, 6]

When depth is unknown

If you do not know how deep the nesting goes, use Infinity:

const unknown = [1, [2, [3, [4, [5]]]]];
unknown.flat(Infinity); // [1, 2, 3, 4, 5]

flatMap() — map and flatten in one step

Array.flatMap() combines map() and flat(1) into a single operation. It is perfect for the case where your mapping function returns arrays.

const sentences = ["hello world", "foo bar"];
sentences.flatMap(s => s.split(" "));
// ["hello", "world", "foo", "bar"]

This is both more readable and more performant than sentences.map(...).flat() because it only makes one pass over the data.

Compatibility note: flat() and flatMap() are supported in all modern browsers and Node.js 11+. If you target older environments, use one of the approaches below.


Approach 2: reduce() + concat() — one level

Before flat() existed, this was the idiomatic way to flatten one level of nesting. It is worth understanding because it appears frequently in older codebases and in interviews.

function flattenOneLevel(arr) {
  return arr.reduce((acc, val) => acc.concat(val), []);
}

flattenOneLevel([1, [2, 3], [4, 5]]); // [1, 2, 3, 4, 5]

Step by step, here is what happens:

Start:    acc = []
Step 1:   acc.concat(1)      → [1]
Step 2:   acc.concat([2, 3]) → [1, 2, 3]
Step 3:   acc.concat([4, 5]) → [1, 2, 3, 4, 5]

concat handles both single values and arrays correctly, which is why this pattern works cleanly. The limitation is that it only flattens one level — if an element is [2, [3, 4]], the [3, 4] stays nested.


Approach 3: Spread operator + concat() — one level

A slightly more concise version of the same idea:

function flattenOneLevel(arr) {
  return [].concat(...arr);
}

flattenOneLevel([1, [2, 3], [4, 5]]); // [1, 2, 3, 4, 5]

The spread operator unpacks arr into individual arguments to concat. Since concat accepts both arrays and non-arrays, this has the same effect as the reduce version. It is terse and readable for one-level flattening.


Approach 4: Recursion — any depth

When you need full depth flattening and cannot use flat(Infinity) — or when an interviewer wants to see you implement it yourself — recursion is the natural solution.

function flattenDeep(arr) {
  return arr.reduce((acc, val) => {
    if (Array.isArray(val)) {
      return acc.concat(flattenDeep(val));
    }
    return acc.concat(val);
  }, []);
}

flattenDeep([1, [2, [3, [4, [5]]]]]); // [1, 2, 3, 4, 5]

The logic is:

  • Walk through each element.

  • If it is an array, recurse into it (flatten it the same way).

  • If it is a plain value, add it to the accumulator.

You can also write this more concisely with flatMap:

function flattenDeep(arr) {
  return arr.flatMap(val =>
    Array.isArray(val) ? flattenDeep(val) : val
  );
}

Both do the same thing. The second form is cleaner once you are comfortable with flatMap.


Approach 5: Iterative with a stack — no recursion

Recursive solutions can hit the call stack limit on extremely deep arrays. The iterative approach using a stack avoids this entirely. This is the solution that impresses in technical interviews.

function flattenIterative(arr) {
  const stack = [...arr];
  const result = [];

  while (stack.length > 0) {
    const val = stack.pop();
    if (Array.isArray(val)) {
      stack.push(...val);
    } else {
      result.push(val);
    }
  }

  return result.reverse();
}

flattenIterative([1, [2, [3, [4]]]]); // [1, 2, 3, 4]

Here is how it works step by step on [1, [2, [3]]]:

Stack:   [1, [2, [3]]]   result: []
Pop [2, [3]] → it's an array → push 2 and [3] back
Stack:   [1, 2, [3]]     result: []
Pop [3]      → it's an array → push 3 back
Stack:   [1, 2, 3]       result: []
Pop 3        → plain value  → result: [3]
Pop 2        → plain value  → result: [3, 2]
Pop 1        → plain value  → result: [3, 2, 1]
Reverse:                     result: [1, 2, 3]

Because we use pop (LIFO), values come out in reverse order — hence the .reverse() at the end. This approach handles arrays of any depth without ever growing the call stack.


Approach 6: toString() — the hacky one

You will occasionally see this trick in the wild. It works only for arrays of primitive values:

[1, [2, [3, [4]]]].toString().split(",").map(Number);
// [1, 2, 3, 4]

Every element gets stringified and joined with commas, then split back apart and converted to numbers. It is clever for specific cases, but it falls apart the moment your array contains objects, null, or strings with commas. Do not use it in production. Know it so you can explain why it is problematic.


All Approaches at a Glance

Approach Depth Notes
arr.flat(n) Any Cleanest; ES2019+
arr.flatMap(fn) 1 Map + flatten in one pass
reduce + concat 1 Pre-ES2019 standard
Spread + concat 1 Concise one-liner
Recursion Any Readable; call stack risk
Iterative (stack) Any Safe; best for deep arrays
toString + split Any Primitives only; fragile

Common Interview Scenarios

Interviewers test array flattening because it exercises several things at once: recursion, iteration, edge case handling, and familiarity with the standard library. Here are the scenarios you should be ready for.


Scenario 1: "Implement flat() without using flat()"

This is the most common prompt. The expected answer is a recursive approach:

function myFlat(arr, depth = 1) {
  if (depth === 0) return arr.slice();

  return arr.reduce((acc, val) => {
    if (Array.isArray(val)) {
      return acc.concat(myFlat(val, depth - 1));
    }
    return acc.concat(val);
  }, []);
}

myFlat([1, [2, [3]]], 1);        // [1, 2, [3]]
myFlat([1, [2, [3]]], 2);        // [1, 2, 3]
myFlat([1, [2, [3]]], Infinity); // [1, 2, 3]

Notice how depth decrements on each recursive call. When it hits 0, recursion stops and the remaining structure is returned as-is. Handling Infinity correctly without infinite looping works because Infinity - 1 === Infinity and the recursion terminates naturally when there are no more arrays to unwrap.


Scenario 2: "Flatten without recursion"

The follow-up is usually: "Now do it iteratively." This tests whether you can translate a recursive algorithm into a loop.

function flatIterative(arr) {
  const stack = [...arr];
  const result = [];

  while (stack.length) {
    const item = stack.pop();
    if (Array.isArray(item)) {
      stack.push(...item);
    } else {
      result.push(item);
    }
  }

  return result.reverse();
}

Walk through it with a small example on the whiteboard. Interviewers want to see you reason about it, not just produce the answer.


Scenario 3: "Flatten and remove duplicates"

A practical follow-up: after flattening, return only unique values.

function flatUnique(arr) {
  return [...new Set(arr.flat(Infinity))];
}

flatUnique([1, [2, 2], [3, [1, 4]]]);
// [1, 2, 3, 4]

Set automatically removes duplicates. Spreading it back into an array gives you the clean result. You can also chain this manually if the interviewer wants no built-ins:

function flatUnique(arr) {
  const flat = arr.flat(Infinity);
  return flat.filter((val, index) => flat.indexOf(val) === index);
}

Scenario 4: "Flatten an array of objects"

Flattening is not always about primitives. You might flatten an array of arrays of objects:

const nested = [
  [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }],
  [{ id: 3, name: "Carol" }]
];

nested.flat();
// [{ id: 1 }, { id: 2 }, { id: 3 }]

Or extract specific fields while flattening using flatMap:

const names = nested.flatMap(group => group.map(u => u.name));
// ["Alice", "Bob", "Carol"]

Scenario 5: "Flatten with a depth limit and count operations"

Occasionally you will be asked to flatten only to a certain depth and track how many elements were processed — testing whether you understand the algorithm, not just the API.

function flatWithCount(arr, depth = Infinity) {
  let count = 0;

  function recurse(a, d) {
    return a.reduce((acc, val) => {
      count++;
      if (Array.isArray(val) && d > 0) {
        return acc.concat(recurse(val, d - 1));
      }
      return acc.concat(val);
    }, []);
  }

  return { result: recurse(arr, depth), count };
}

flatWithCount([1, [2, [3]]], 1);
// { result: [1, 2, [3]], count: 3 }

Edge Cases Worth Knowing

Empty arrays. Flattening an empty array returns an empty array. No special handling needed.

[].flat(); // []
[[],[]].flat(); // []

Sparse arrays. flat() removes empty slots:

[1, , 3].flat(); // [1, 3]

Mixed types. Flattening works on any mix of types — numbers, strings, objects, booleans. Values are preserved as-is.

[1, ["hello"], [true, null]].flat();
// [1, "hello", true, null]

Already flat. Calling flat() on a flat array returns a shallow copy. It does not throw.

[1, 2, 3].flat(); // [1, 2, 3]  — a new array

Putting It Together: A Real-World Example

Suppose you are building a tag system. Each post has an array of tags, and you fetch multiple pages of posts:

const pages = [
  [{ title: "Post A", tags: ["js", "web"] }, { title: "Post B", tags: ["css"] }],
  [{ title: "Post C", tags: ["js", "node"] }]
];

const allPosts = pages.flat();
const allTags = [...new Set(allPosts.flatMap(post => post.tags))];

console.log(allTags); // ["js", "web", "css", "node"]

Step by step:

  • pages.flat() collapses the paginated chunks into a single list of posts.

  • flatMap(post => post.tags) extracts every tag from every post, flattening the resulting array of arrays.

  • new Set(...) removes duplicates, and spreading it gives a clean array.

Two lines of readable, declarative code to solve a genuinely common problem.


Wrapping Up

Flattening arrays is a small skill with a wide reach. Once you understand the concept — removing layers of nesting to get at the values inside — you can apply it to almost any data shape you encounter.

Start with flat() and flatMap() for everyday use. Understand the reduce-based approach for compatibility. Know the recursive and iterative implementations cold if you are preparing for interviews. And always think about depth — flat by one level versus all the way down are meaningfully different operations.

The flattening problem looks simple on the surface, which is exactly why it shows up in interviews: the implementation reveals whether you truly understand arrays, recursion, and iteration, or whether you are just calling built-ins without knowing what is underneath.

No comments yet. Be the first to comment.

More from this blog

Flattening Arrays in JavaScript: A Complete Guide with Interview Scenarios