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.
Comments
Join the discussionNo comments yet. Be the first to comment.
