Skip to main content

Array Flatten in JavaScript

Updated 18 min readMarkdown
Array Flatten in JavaScript

A nested array is an array that contains other arrays as elements. Flattening means converting this multi-dimensional structure into a single-level array. In practice, flattening arrays is very useful –

for example,
converting complex JSON data into a simple list for tables, charts or search indexes. It makes loops, filters, sorts, and searches much easier. In this guide, we’ll start with clear problem statements, use visual diagrams to explain nested structures, and walk through several methods (with code and complexity notes) to flatten arrays. We’ll cover built-in methods, recursion, iteration, generators, utility libraries, and discuss edge cases like sparse arrays and circular references.

What Is a Nested Array?

A nested array is simply an array that contains one or more arrays inside it. Think of it like boxes inside boxes. For example:

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

Here, nested[1] is the array [2, 3], and nested[2] is another array [4, [5, 6]] which itself contains the array [5, 6]. Visually, it looks like a tree:

graph LR
    A[[Root Array [1, [2,3], [4,[5,6]]]]] --> B[1]
    A --> C["[2,3]"]
    A --> D["[4,[5,6]]"]
    C --> E[2]
    C --> F[3]
    D --> G[4]
    D --> H["[5,6]"]
    H --> I[5]
    H --> J[6]

Figure: A nested array ([1,[2,3],[4,[5,6]]]) shown as a tree – the root array has sub-arrays, which themselves contain values or further sub-arrays.

By contrast, a flat array has no nested arrays: all elements are simple values (numbers, strings, objects, etc.). For example, the flattened version of the above would be [1,2,3,4,5,6].

Definition: Flattening a multi-dimensional array means converting it into a one-dimensional (flat) array by replacing each nested array with its contents.

This concept appears in many real-world situations. For instance, an API might return hierarchical categories, but your UI needs a flat list of tags. Or you may have nested comment threads that you want to process as a single list. Flattening is the operation that bridges these differences, turning a tree of data into a linear array.

Why Flattening Arrays Is Useful

Flattening isn’t just a technical trick – it solves practical problems. As one developer notes, you often see deeply nested data from an API that must be converted into a flat list for use in your application. It also normalizes data: many libraries (for charts, tables, search indexes, etc.) expect flat data structures.
For example:

  • Data Processing: Sorting, filtering, and searching are simpler on a 1D array than on nested structures.

  • User Interfaces: A UI might display a list of items (e.g. product tags), requiring flattening of nested categories.

  • Algorithms: Many algorithms work more efficiently on a simple list. For instance, removing duplicates or calculating aggregates is easier after flattening.

  • Real Examples: E-commerce sites often flatten product categories and sub-categories into tag lists. Analytics might flatten event hierarchies. Forms may flatten nested error messages into a single list.

In short, flattening “makes operations trivial” on a flat array. It’s a skill that separates a beginner (who just makes it work) from an experienced developer (who designs clean solutions).

How Flattening Works (Core Concept)

Imagine flattening as a tree traversal. The recursive idea is:

For each element in the array:

  • If it’s not an array, add it to the result.

  • If it is an array, flatten that array first (recursively), then add its contents to the result.

This approach “beautifully mirrors how tree traversal works”. The result is a single list of all leaf values.

There are two main variations:

  • Shallow flatten: Only flatten one level deep (merge sub-arrays into root, but do not flatten deeper nested arrays).

  • Deep flatten: Flatten all levels (convert entire nested structure into one list).

Many algorithms iterate and apply this rule.
For example, with our nested = [1, [2, [3, [4]]], 5], a deep flatten yields [1, 2, 3, 4, 5].

Here’s a simple ASCII diagram showing a flatten transformation:

Before: [1, [2, [3, 4]], 5]
                 |
              [3,4]  (sub-array inside second element)
                 
Flatten each level:
    1. Inspect 1 → not an array → keep [1]
    2. Inspect [2,[3,4]] → is array → flatten it:
           - inspect 2 → [2]
           - inspect [3,4] → flatten to [3,4]
       combined result → [2, 3, 4]
    3. Inspect 5 → [5]
        
After: [1, 2, 3, 4, 5]

Or in a compact form:

Nested: [1, [2, [3, 4]], 5]  
Flatten → [1, 2, 3, 4, 5]

This step-by-step reasoning – checking each element, handling numbers vs arrays – is key to flattening, and is exactly how we'd explain this to an interviewer or beginner.

Approaches to Flatten Arrays (with Code)

There are many ways to flatten arrays in JavaScript. We’ll cover several approaches:

  • Built-in method (flat) – ES2019+

  • Recursion (manual loop)

  • Functional (reduce + concat)

  • Iterative (stack or queue)

  • Generators

  • Utility libraries (lodash)

Below is a summary of these approaches (code, time/space complexity, pros/cons):

Method Example Code Snippet Time / Space Pros Cons
Array.prototype.flat const flat = nestedArray.flat(Infinity);【7†L214-L215】 O(N) / O(N) One-liner, optimized, depth param Requires ES2019+ support
Simple Recursion js<br>function flatten(arr){<br> let res=[];<br> for(let x of arr){<br> if(Array.isArray(x)) res.push(...flatten(x));<br> else res.push(x);<br> }<br> return res;<br>} ≈O(N) / O(N) Easy logic, handles any depth Can hit call-stack on huge depth; intermediate arrays make it O(N log N) worst-case
reduce + concat js<br>const flatten = arr => arr.reduce((a, x) =><br> a.concat(Array.isArray(x) ? flatten(x) : x), []);【13†L192-L195】 ≈O(N) / O(N) Concise, functional style Similar recursion cons (stack overflow)
Iterative (Stack) js<br>function flatten(arr) {<br> let res=[], stack=[...arr];<br> while(stack.length){<br> let x = stack.pop();<br> if(Array.isArray(x)) stack.push(...x);<br> else res.unshift(x);<br> }<br> return res;<br>}【13†L207-L215】 O(N) / O(N) No recursion (avoids call-stack limit) Slightly more code; uses extra stack array
Generator Function js<br>function* flatGen(arr) {<br> for(let x of arr){<br> if(Array.isArray(x)) yield* flatGen(x);<br> else yield x;<br> }<br>}<br>const result = [...flatGen(nested)]; O(N) / O(N) (for output) Elegant lazy evaluation, re-usable iterator Syntax complexity, less common
Lodash _.flattenDeep _.flattenDeep(nested); (from Lodash)【28†L348-L355】 O(N) / O(N) One call, handles any depth Requires including Lodash library
Lodash _.flatten _.flatten(nested);【31†L348-L355】 O(N) / O(N) Flattens one level, easy Only one level (not deep)
  • Time Complexity: Generally each method touches each array element once, so O(N) where N is total number of elements across all levels. (However, note [38] that naive recursion with concatenation may degrade to O(N log N) in worst-case balanced nesting.

  • Space Complexity: All these methods create a new flat array, so O(N) extra space. Recursive or generator approaches also use O(depth) stack space.

Let’s look at these in more detail:

1. Built-in Array.prototype.flat()

ES2019 introduced flat(), which flattens an array to a given depth【7†L214-L215】. For example:

const nested = [1, [2, 3], [4, [5, 6]]];
console.log(nested.flat());       // [1, 2, 3, 4, [5, 6]]
console.log(nested.flat(Infinity)); // [1, 2, 3, 4, 5, 6]
  • By default, flat() flattens one level deep (like calling it with 1 depth). You can pass a depth parameter, even Infinity to flatten all the way.

  • It returns a new array and does not modify the original.

  • It also removes empty slots in sparse arrays. For example, [1, , 3].flat() yields [1, 3]. (Note: holes are removed, but values like undefined remain.

  • This method is highly optimized in modern engines and is usually the simplest solution for daily use.

Pros: Very concise, well-supported in modern environments, and optimized by the JS engine.
Cons: Not available in very old JS environments (IE). Also, shallow vs deep must be set explicitly.

2. Recursive Flattening (Loop + Recursion)

A classic interview solution is a recursive function:

function flattenRecursive(arr) {
  let result = [];
  for (let item of arr) {
    if (Array.isArray(item)) {
      // Recurse into sub-array
      result = result.concat(flattenRecursive(item));
    } else {
      result.push(item);
    }
  }
  return result;
}

// Example:
console.log(flattenRecursive([1, [2, [3, 4]], 5])); // [1,2,3,4,5]

Here’s the step-by-step logic:

  1. Start with an empty result array.

  2. Loop through each element item in the input arr.

  3. If item is not an array, push it to result.

  4. If item is an array, call flattenRecursive(item) and concat its returned values into result.

This directly implements the “if array flatten it first” rule. It works for any depth of nesting.

Pros: Clear logic; directly shows the recursive thought process (often preferred in interviews).
Cons: Can hit maximum call-stack for very deep arrays. Also, each recursive concat creates a new array, so in some pathological cases the runtime can be worse than linear (on the order of O(N log N) in a balanced nesting). In practice, for normal data this is usually fine.

Complexity: Each element is processed once, so O(N) time in most cases. However, as explains, the use of concat() can introduce extra overhead (master theorem yields O(N log N) in the worst nested-case). Space is O(N) as we build a new array.

3. Functional Style with reduce() + concat()

You can flatten in a functional style using reduce():

const flattenReduce = (arr) =>
  arr.reduce((acc, item) =>
    acc.concat(Array.isArray(item) ? flattenReduce(item) : item),
    []
  );

console.log(flattenReduce([1, [2, [3, 4]], 5])); // [1,2,3,4,5]

This approach is essentially the same recursion, but using reduce to accumulate results. It checks each item: if it’s an array, it recursively flattens it; otherwise it concatenates the item.

Pros: Elegant one-liner (aside from defining the function). Shows off functional programming knowledge.
Cons: The underlying performance and recursion pitfalls are the same as the simple recursion above. Also, beginners may find the reduce callback syntax a bit tricky.

4. Iterative Flatten (Using a Stack or Queue)

For very deep nesting, recursion might overflow the call stack. An iterative approach can avoid recursion entirely. One method uses a stack:

function flattenIterative(arr) {
  const result = [];
  const stack = [...arr];  // copy initial array

  while (stack.length) {
    const next = stack.pop();
    if (Array.isArray(next)) {
      // Push all items of sub-array into stack
      stack.push(...next);
    } else {
      // Since we're popping from end, unshift to result front
      result.unshift(next);
    }
  }

  return result;
}

console.log(flattenIterative([1, [2, [3, 4]], 5])); // [1,2,3,4,5]
  • We copy the array into a stack.

  • While the stack is not empty, pop an element: if it’s an array, push its elements onto the stack; otherwise, insert it at the front of result.

  • In the end, result holds the flattened elements in original order. (We used unshift, which reverses order because we popped from the end. Alternatively, one could push and then result.reverse() at the end.)

Pros: No recursion, so you avoid call-stack limits. Good interview mention as a non-recursive alternative.
Cons: Slightly more complex to write. Uses extra stack space.

5. Generators (ES6 Iterators)

Another advanced method is to use a generator function. This can be useful if you want to lazily flatten:

function* flattenGen(arr) {
  for (let x of arr) {
    if (Array.isArray(x)) {
      yield* flattenGen(x);
    } else {
      yield x;
    }
  }
}

const nested = [1, [2, [3, 4]], 5];
const flattenedArray = [...flattenGen(nested)];
console.log(flattenedArray); // [1,2,3,4,5]

The flattenGen function recursively yields values. We use yield* to yield values from a sub-generator when we encounter an array. Finally, we spread the generator into an array.

Pros: Clean, lazy evaluation. You can even process the flattened items one by one without building a full array.
Cons: More complex syntax, and for many beginners this is advanced. The runtime behavior is similar to recursive flatten.

6. Utility Libraries (Lodash)

If you’re using a utility library like Lodash, there are built-in methods:

  • _.flatten – flattens one level of array.

  • _.flattenDeep – flattens all levels (deep flatten).

  • _.flattenDepth – flattens to a specified depth.

For example:

_.flatten([1, [2, [3]], 4]);       // [1, 2, [3], 4]
_.flattenDeep([1, [2, [3]], 4]);  // [1, 2, 3, 4]

Pros: Very convenient and well-tested.
Cons: Requires loading Lodash, which may be overkill if that’s the only utility needed.

Depth: Fixed vs Arbitrary

Some flatten functions allow specifying a depth. For instance, flat(2) only flattens two levels. The built-in flat() does this; Lodash’s flattenDepth does the same.

  • Fixed depth: Sometimes you only want to flatten a certain number of levels, not all the way. Example: nestedArray.flat(1) will only merge one level of sub-arrays.

  • Arbitrary depth (full flatten): Often you want all levels. Using Infinity with flat() (or _.flattenDeep) achieves this. Or simply write the recursive approach which naturally goes to any depth.

Choosing depth is often a requirement clarification in interviews: make sure you know whether to flatten one level or all levels. Always clarify this: for instance, ask “Should I flatten just one level or fully flatten?”.

Order and Types: What Gets Preserved

Flattening preserves the original order of elements (albeit linearly) and does not change the elements themselves. For example, if there are null or undefined values, they remain (except that flat() removes holes). Sparse arrays (with “empty slots”) have those slots removed by flat(). Other types like objects or strings are just carried along if present.

Example: [1, [2, null], undefined, [3, [4, []]]] flattened (deep) becomes [1, 2, null, undefined, 3, 4]. Notice that the empty array [] just contributes no elements.

Edge Cases

When flattening, watch out for these special scenarios:

  • Empty or already-flat arrays: If the input has no nested arrays, just return it (or a shallow copy) as-is.

  • Negative or excessive depth (if using flat): If someone passes an invalid depth (like negative), it’s common to treat it as 0 (no flatten) or Infinity. By default, flat() treats negative depth as 0 (no change).

  • Sparse arrays: flat() removes holes (see above. But manual methods (forEach, for...of) will ignore empty slots anyway, so this usually isn’t an issue unless you explicitly try to preserve length.

  • Circular references: If the nested structure refers to itself (e.g. a = [1]; a.push(a);), then naive recursive flattening will loop infinitely or crash. Most simple flatten functions do not handle circular structures – they will RangeError: Maximum call stack or run forever. To handle this, you’d need to track seen objects. Interview tip: always clarify if circular references need handling. Usually the task of flattening plain arrays assumes no circular refs.

In interviews, you might be asked about edge cases explicitly. For example, “What if the array has no nesting?” or “How does your function handle [ [], [ ] ] or [null, [undefined, [ ]]]?” It’s good to mention how your solution deals with empty sub-arrays (they just add nothing) and null/undefined (they stay as values, not ignored).

Common Interview Scenario – “Flattening” Questions

A typical interview question is: “Flatten an array of nested arrays into a single array” without using the built-in flat(). They’ll look for your problem-solving approach and edge-case thinking. Steps to ace it:

  1. Clarify Requirements: Ask if it’s shallow vs deep flattening, and what to do with edge cases (empty arrays, non-arrays, etc.).

  2. Start Simple: Outline the recursive idea: “For each element, if it’s an array, flatten it recursively, else keep it.”

  3. Code Iteratively: Write the recursive or iterative code as shown above. Explain each step (loop through, check Array.isArray()).

  4. Test with Examples: Walk through a concrete example, like [1, [2, [3, 4]], 5]. Show how the recursion or loop handles each element.

  5. Analyze Complexity: Mention it’s roughly O(N) time (each element once) and O(N) space for the output. If asked, acknowledge the subtlety from [38] that naive concatenation can give O(N log N) in a perfectly balanced tree shape, whereas a single-pass approach can be strictly O(N).

  6. Iterative Alternative: If time permits, suggest an iterative stack solution to demonstrate depth understanding and avoid recursion depth issues.

  7. Follow-ups: Be prepared for follow-ups: “What if elements aren’t numbers but objects?” (You still keep them as-is). “How do you prevent stack overflow on very deep arrays?” (Use iterative or check depths). “Could your solution modify the original array?” (Typically no, we create a new array).

Interviewers may also ask variant questions, e.g. "Flatten only up to depth 2" or "Implement a Flatten Iterator (like LeetCode 341)". The key is showing you understand recursion vs iteration and can reason about data shape.

Worked Examples

Let’s see some concrete examples, step-by-step.

  1. One-Level Nesting:

    • Input: [1, [2, 3], 4]

    • Goal: Flatten completely → [1, 2, 3, 4].

    • Process:

      1. 1 is a number → keep [1].

      2. [2, 3] is an array → flatten it (one level): it yields 2, 3. Append to result → [1, 2, 3].

      3. 4 is a number → append → [1, 2, 3, 4].

    • Result: [1, 2, 3, 4].

    • (Using flat(Infinity) or recursion gives the same result.)

  2. Two-Level Nesting:

    • Input: [1, [2, [3, 4], 5], 6]

    • Process:

      1. 1 → [1].

      2. [2, [3, 4], 5] → array, flatten it:

        • 2 → add 2 → [1, 2].

        • [3, 4] → flatten it: 3, 4 → add → [1, 2, 3, 4].

        • 5 → add → [1, 2, 3, 4, 5].

      3. 6 → add → [1, 2, 3, 4, 5, 6].

    • Result: [1, 2, 3, 4, 5, 6].

  3. Mixed Types & Empty Arrays:

    • Input: [0, [], [1, [2, []], null], undefined]

    • Goal: Flatten completely.

    • Process:

      1. 0 → [0].

      2. [] (empty array) → flatten (yields nothing) → still [0].

      3. [1, [2, []], null]:

        • 1 → add → [0, 1].

        • [2, []]:

          • 2 → add → [0,1,2].

          • [] → yields nothing.

        • null → add → [0, 1, 2, null].

      4. undefined → add → [0, 1, 2, null, undefined].

    • Result: [0, 1, 2, null, undefined].

Each step we keep order. Note that holes (the empty arrays []) just add no elements, and null/undefined remain in the output.

Walkthrough Example (Recursion): For [1, [2, [3, 4], 5], 6] with recursion:

flatten([1, [2, [3,4], 5], 6])
= [1].concat(flatten([2, [3,4], 5])).concat([6])
flatten([2, [3,4], 5])
= [2].concat(flatten([3,4])).concat([5])
flatten([3,4])
= [3, 4]  // base case (no arrays inside)
=> Combine: [2].concat([3,4]).concat([5]) = [2,3,4,5]
=> Combine: [1].concat([2,3,4,5]).concat([6]) = [1,2,3,4,5,6]

Practice Problems

Try these on your own and check the answers:

  1. Problem: Flatten [[1, 2], 3, [4, [5]], 6].
    Answer: [1, 2, 3, 4, 5, 6].
    Explanation: Merge inner arrays: [1,2] → 1,2; [4,[5]] → 4, flatten [5] → 5.

  2. Problem: Write a function flatten(arr) that only flattens one level (shallow flatten). Example: [1, [2, [3]], 4] → [1, 2, [3], 4]. What does flatten([1, [2, [3]], 4]) return?
    Answer: [1, 2, [3], 4] (only the first [ is flattened).
    Explanation: One-level flatten just removes one set of brackets.

  3. Problem: Given const data = [1, [2, [3, [4, [5, 6]]]], 7], what does data.flat(2) and data.flat(Infinity) produce?
    Answer:

    • data.flat(2) → [1, 2, 3, [4, [5,6]], 7] (only flattens two levels).

    • data.flat(Infinity) → [1, 2, 3, 4, 5, 6, 7] (completely flat).
      Explanation: flat(2) merges the first two levels of nesting but leaves the deeper [4,[5,6]] partially nested.

  4. Problem: (Edge case) What happens with an empty array or no nesting? Flatten [] or [1,2,3].
    Answer: Flattening yields the same array (or a copy). e.g. [].flat() → []; [1,2,3].flat() → [1,2,3].
    Explanation: There’s nothing to flatten.

  5. Problem: (Tricky) Given a sparse array like [1, , [3, ,4], 5], what does flat() do?
    Answer: It removes holes: [1, 3, 4, 5]【36†L300-L308】.
    Explanation: The empty slots are removed by flat()【36†L300-L308】.

If you’re studying, try writing the flatten function yourself for some practice cases like these.

Summary and Best Practices

  • Understand the problem: A nested array is just an array containing arrays. Flattening means turning it into one flat array.

  • Use built-ins when possible: arr.flat(depth) is concise and efficient. For older environments, you can polyfill or use a library like Lodash (_.flattenDeep).

  • Know multiple approaches: Recursion is straightforward and often expected. Iterative or generator methods show deep understanding. Each approach has its trade-offs (stack depth vs code length).

  • Think about edge cases: Explain how your code handles empty arrays, holes, non-array elements, and circular references.

  • Maintain readability: Even if using recursion or advanced methods, write clean code with clear variable names. Commenting the logic (as we did above) helps beginners and interviewers follow your thought process.

  • Complexity matters: Be prepared to analyze your solution. Simple flatten is typically O(N) time and space. If concatenating arrays in recursion, mention the caveat that it can become O(N log N) in balanced cases, and suggest improvements if needed.

  • Use diagrams and examples: As we’ve shown, drawing a quick sketch of the nested array or writing a small example can make the flattening process crystal clear.

By following these guidelines and trying out the approaches above, beginners will not only flatten arrays but also understand why each step works. Practice writing out the steps or diagrams for new examples to solidify the concept. Happy coding!

10 views

No comments yet. Be the first to comment.