Flatten an array without flat()

To flatten an array without flat(), walk it and recurse into any element that’s itself an array, pushing everything else straight into the result. Use Array.isArray to decide, and build a new array so the input is never modified.

Easy to hard recursion nested arrays

The question

The setup is always "flatten this to any depth, and no, you can’t use flat()". What’s actually being checked is narrower than it looks: whether you recurse to arbitrary depth rather than one level, whether you leave the input alone, and whether falsy values survive or a stray filter eats them.

The solution

The recursive version is the one to write first. It’s short enough to be obviously correct, and an interviewer who wants the iterative version will ask for it.

function flatten(value) {
  const result = [];

  for (const item of value) {
    if (Array.isArray(item)) {
      // Spread the recursive result rather than pushing the array itself.
      result.push(...flatten(item));
    } else {
      // Everything else is a value, including null, undefined, 0 and "".
      result.push(item);
    }
  }

  return result;
}

How it works

Array.isArray is the only test you need. Anything that isn’t an array is a value, objects included, and spreading the recursive result is what merges nested values in rather than nesting the array itself.

Nothing gets filtered, so 0, null, undefined, false and "" all survive. Empty nested arrays vanish on their own, because recursing into one produces nothing to push. Objects keep their identity too: values are moved, never copied, so a shared object turns up twice as the same reference.

The tests it has to pass

This is the part most explanations leave out. "It works" is not a claim you can check by reading. These are the 8 cases Practice Pad runs against your solution, and the implementation above passes all of them.

Easy3 cases · depth and immutability

flattens values at every depth

expectEqual(flatten([1, [2, [3]], 4]), [1, 2, 3, 4]);

returns a flat array unchanged

expectEqual(flatten([1, 2, 3]), [1, 2, 3]);

returns a new array rather than the one it was given

const source = [1, [2]];
expectEqual(flatten(source) === source, false);
Medium3 cases · deep nesting

drops empty nested arrays without dropping values

expectEqual(flatten([["a"], [], ["b", ["c"]]]), ["a", "b", "c"]);

flattens arrays nested many levels deep

expectEqual(flatten([1, [2, [3, [4, [5, [6]]]]]]), [1, 2, 3, 4, 5, 6]);

leaves the original array unchanged

const source = [1, [2]];
flatten(source);
expectEqual(source, [1, [2]]);
Hard2 cases · references and falsy values

keeps objects as their original references

const shared = { id: 7 };
const flattened = flatten([[shared], [[shared]]]);
expectEqual(flattened.length, 2);
expectEqual(flattened[0] === shared && flattened[1] === shared, true);

keeps null, undefined, zero, and empty strings as values

expectEqual(flatten([0, [null, [undefined]], false, ""]), [0, null, undefined, false, ""]);

Edge cases to watch out for

Filtering falsy values by accident

Reaching for .filter(Boolean) to drop empty arrays also drops 0, "", null, undefined and false. Empty arrays need no special handling at all: recursing into one contributes nothing.

Flattening only one level

[].concat(...value) is a neat one-liner and flattens exactly one level. It passes the simplest test and fails on [1, [2, [3]]].

Mutating the input

Using splice to flatten in place is a legitimate approach, but the question asks for a new array and one of the medium tests checks the original is unchanged. If you flatten in place, say so and offer the alternative.

If asked for an iterative version, use an explicit stack: push the array onto it, pop items, and push nested arrays back on rather than recursing. That avoids blowing the call stack on pathologically deep input, which is the usual reason an interviewer asks.

Follow up questions

How do you flatten only a set number of levels?

Pass the depth down and decrement it: recurse only while depth > 0, and push the element as-is once it hits zero. That gives you the same signature as the built-in, where flat() defaults to one level and flat(Infinity) goes all the way.

How do you flatten an array using reduce?

value.reduce((acc, item) => acc.concat(Array.isArray(item) ? flatten(item) : item), []). It’s the same algorithm in a different shape. The explicit loop is usually easier to explain out loud, and concat in a reduce builds a new array on every step.

What does flat() do by default?

It flattens exactly one level. flat(2) goes two levels deep, and flat(Infinity) flattens completely, which is the behaviour a from-scratch implementation is usually asked to match.

Does flattening copy the nested objects?

No. The values are moved into a new array, so objects keep their original identity and a shared object appears at multiple positions as the same reference. Copying them would be a deep clone, which is a different question.

Practise this with the tests running

Practice Pad runs these 8 cases against your own solution, in a real editor, on your Mac. Twenty-four questions, and an optional AI panel that explains why a test failed.

Download for macOS Free. Requires macOS 12 or later on Apple Silicon.

See all 24 questions