If you just need to flatten an array in production code, use the built-in Array.prototype.flat:
const nested = [1, [2, [3, [4, [5]]]]];// Flatten one level (the default).nested.flat(); // [1, 2, [3, [4, [5]]]]// Flatten a specific number of levels.nested.flat(2); // [1, 2, 3, [4, [5]]]// Flatten all levels by passing Infinity.nested.flat(Infinity); // [1, 2, 3, 4, 5]
Array.flat is part of ES2019 and is supported in every evergreen browser and Node.js 11+. It returns a new array and does not mutate the original.
Array.flat isn't enoughArray.flat covers the standard case. You'll need a custom flatten when you need any of the following:
Buffer-like objects or typed arrays intact while flattening plain arrays.For all of those, you'll write your own flatten. This is also one of the most common JavaScript interview questions because it tests recursion, type checking, and array manipulation in a small surface area.
Implement a function flatten that returns a newly created array with all subarray elements concatenated recursively into a single level.
// Single-level arrays are unaffected.flatten([1, 2, 3]); // [1, 2, 3]// Inner arrays are flattened into a single level.flatten([1, [2, 3]]); // [1, 2, 3]flatten([[1, 2],[3, 4],]); // [1, 2, 3, 4]// Flattens recursively.flatten([1, [2, [3, [4, [5]]]]]); // [1, 2, 3, 4, 5]
This is a common JavaScript interview question because it tests a small but transferable recursive condition: every item is either already a leaf value to keep, or it is another array whose leaves should be appended in the same left-to-right order.
At every level, flatten repeats the same decision: if the next item is not an array, append it to the output; if it is an array, open it and process its items. The approaches below differ in where the still-unprocessed items live: an explicit working array, the call stack, or the input array itself.
For the recursive solution, the useful trace is:
| Input chunk | Decision | Contribution |
|---|---|---|
1 | leaf | [1] |
[2, [3]] | nested array | flatten it first |
2 | leaf inside nested array | [2] |
[3] | nested array | [3] |
| combined | concatenate contributions | [1, 2, 3] |
First, decide how to check whether a value is an array. Use Array.isArray or instanceof Array for that check. There are some nuances between these two. For more detail, see this article by Jake Archibald.
Then, avoid mutating the original input array. Make a copy of the input array and return a new array with all the items extracted from it.
This solution loops through the array with a while loop, takes out one item at a time, and checks whether that item is an array. If it is not an array, put it into the res array. If it is an array, use the spread operator ... to get all the items out of it and put them back into the array.
type ArrayValue = any | Array<ArrayValue>;export default function flatten(value: Array<ArrayValue>): Array<any> {const res = [];// Work on a copy so the queue-style unshift/shift logic does not mutate the input.const copy = value.slice();while (copy.length) {const item = copy.shift();if (Array.isArray(item)) {// Put nested items back at the front so their order is preserved.copy.unshift(...item);} else {res.push(item);}}return res;}
Array.prototype.someA more concise approach, compared to the previous one, is to use Array.prototype.some.
type ArrayValue = any | Array<ArrayValue>;export default function flatten(value: Array<ArrayValue>): Array<any> {// Flatten one level at a time until no nested arrays remain.while (value.some(Array.isArray)) {value = [].concat(...value);}return value;}
Array.prototype.reduceA recursive approach fits well here given the nested nature of this question, and it will simplify the code a lot.
/*** @param {Array<*|Array>} value* @return {Array}*/export default function flatten(value) {return value.reduce(// Recurse only for nested items, then concatenate the flattened chunk.(acc, curr) => acc.concat(Array.isArray(curr) ? flatten(curr) : curr),[],);}
Although a recursive approach always has the risk of overflowing the call stack, as of this writing, in Chrome, the maximum practical recursive depth is around 10 thousand calls, and in Firefox, it is 50 thousand, so this should not be a problem in practice. However, when the cost of recursion becomes a concern, a generator can lazily extract the flattened items from the array. That solution appears later.
All solutions so far return a new flattened array without mutating the original input array. Again, this is normally the desired behavior.
However, the interviewer might ask for an in-place solution that doesn't allocate extra memory. That is, a solution with a constant O(1) space complexity.
In this case, the implementation needs array methods that mutate. There are 9 methods in total that mutate arrays: pop, push, reverse, shift, sort, splice, unshift, copyWithin, and fill.
Here is one possible solution that uses splice to mutate the input array:
type ArrayValue = any | Array<ArrayValue>;export default function flatten(value: Array<ArrayValue>): Array<any> {for (let i = 0; i < value.length; ) {if (Array.isArray(value[i])) {// Stay on the same index because splicing may reveal another nested array there.value.splice(i, 1, ...value[i]);} else {i++;}}return value;}
flatMapThe flatMap method returns a new array formed by applying a given callback function to each element of the array, and then flattening the result by one level. By calling it recursively, the entire array can be flattened until it is only one level deep.
type ArrayValue = any | Array<ArrayValue>;export default function flatten(value: Array<ArrayValue>): Array<any> {// flatMap removes one level, so the recursive call handles arbitrarily deep nesting.return Array.isArray(value) ? value.flatMap((item) => flatten(item)) : value;}
The following solutions only work under certain circumstances, so they should not be used during interviews.
As discussed earlier, if the array has thousands of levels of nesting, a recursive approach might cause a stack overflow. Generators can yield array items individually. Because generators are lazy, this would not have as large an upfront cost compared to the recursive approach.
This, however, results in a different function signature and is not supported by the test cases.
type ArrayValue = any | Array<ArrayValue>;export default function* flatten(value: Array<ArrayValue>,): Generator<any, void, unknown> {for (const item of value) {if (Array.isArray(item)) {// Yield from the nested generator so values stream out lazily.yield* flatten(item);} else {yield item;}}}
JSON.stringifyHere is a solution that might be considered unorthodox: encode the array into a JSON string via JSON.stringify, remove brackets using the regex /(\[|\])/g, and decode it back to an array using JSON.parse. Because all the brackets are stripped off with the regex, the result is an array with only one level of depth.
type ArrayValue = any | Array<ArrayValue>;export default function flatten(value: Array<ArrayValue>): Array<any> {// This only works for JSON-serializable values because it strips brackets from the serialized form.return JSON.parse('[' + JSON.stringify(value).replace(/(\[|\])/g, '') + ']');}
toString when the array contains only numbersRecall the data-types clarification question. If the array contains only numbers, here is a very simple solution:
function flattenOnlyNumbers(array) {return array.toString().split(',').map((numStr) => Number(numStr));}
Note that this only applies when the array contains only numbers, and this usage of toString might be thought of as "obscure".
Array.prototype.flatJavaScript includes a flat array method. It is usually off limits during interviews, but it is still useful to know that a native flatten function exists.
export default function flatten(arr) {return arr.flat(Infinity);}
toString() shortcuts only work for narrow data shapes and can lose types or values.Array.isArray() is the safest ordinary check for array values across realms.console.log() statements will appear here.