You're given an array that may contain nested arrays, nested any number of levels deep. Implement flatten(arr) so it returns a new array with all nesting removed — every value pulled up to a single, flat level. The native version is Array.prototype.flat(Infinity); your job is to write the recursion yourself.
// Returns a new array with every nested array fully unwrapped.
// Does NOT mutate the input.
function flatten(arr: any[]): any[];
flatten([1, 2, 3]); // [1, 2, 3]
flatten([1, [2, 3]]); // [1, 2, 3]
flatten([1, [2, [3, [4]]]]); // [1, 2, 3, 4]
flatten([[[[[5]]]]]); // [5]
flatten([]); // []
flatten([1, [], [2, [3, []]]]); // [1, 2, 3]
flatten([1, 'a', [true, [null]]]); // [1, 'a', true, null]
Array.prototype.flat(Infinity) — there is no depth argument here, you go all the way down.[1, , 3]) are dropped, the same way the native flat drops them. Explicit undefined ([1, undefined, 3]) is kept.Array.isArray to decide. Objects, strings, typed arrays, and array-likes ({ 0: 'a', length: 1 }) stay as-is.arr.flat(). The whole point is to write the recursion. flat, flatMap, and JSON.parse(JSON.stringify(...)) are off-limits.You'll write a function that walks a nested array and pulls every value up to a single, flat level — the recursive twin of Array.prototype.flat(Infinity).
You're handed an array. Most of its slots hold values, but some slots hold more arrays — and those inner arrays can hold more arrays inside them, with no fixed limit on depth. Your job is to produce a new array that contains the same leaf values, in the same left-to-right order, but with all the wrapping arrays gone. Native Array.prototype.flat does this when you pass Infinity; here you implement the recursion by hand.
Picture the input as a tree. Numbers and strings are leaves; arrays are branches. flatten walks the tree depth-first, left to right, and collects every leaf it sees into one flat row. The branch structure is thrown away; only the leaves survive.
The fact that arrays-inside-arrays-inside-arrays is the same shape as just one array-inside-an-array is the cue that recursion fits: the problem of "flatten this" is solved by solving "flatten this smaller thing" and gluing the answer in.
If you've only seen one level of nesting, you might write the version that handles exactly that — peel off the outer wrapper, push each inner element in:
function flattenOneLevel(arr) {
const result = [];
for (const value of arr) {
if (Array.isArray(value)) {
for (const inner of value) result.push(inner);
} else {
result.push(value);
}
}
return result;
}
For [1, [2, 3], 4] this returns [1, 2, 3, 4] — looks right. Try [1, [2, [3, [4]]]] and you get [1, 2, [3, [4]]]. The function lifts the first layer of nesting but drops anything below as-is. We need to keep peeling until there's nothing left to peel.
The fix is to apply flatten to the inner array too — and let its recursion handle whatever depth is in there.
function flatten(arr) {
const result = [];
for (let i = 0; i < arr.length; i++) {
// Skip sparse holes. `i in arr` is false for [1, , 3] at i=1
// but TRUE for [1, undefined, 3] — matches Array.prototype.flat's behavior.
if (!(i in arr)) continue;
const value = arr[i];
if (Array.isArray(value)) {
// Recurse: trust that flatten(value) returns a fully flat array,
// then push each of its elements in. Don't push the inner array itself.
const inner = flatten(value);
for (let j = 0; j < inner.length; j++) {
result.push(inner[j]);
}
} else {
// Leaf value (number, string, object, etc.) — keep as-is.
result.push(value);
}
}
return result;
}
module.exports = { flatten };
Three things separate this from the naive version. First, Array.isArray(value) followed by flatten(value) instead of an inner for loop — that recursive call is the entire difference. Second, an indexed for loop plus i in arr to handle sparse holes — for..of and forEach walk holes inconsistently across engines, so we hand-roll the check. Third, result.push(inner[j]) one-by-one rather than result.push(...inner) — spreading works, but on very large arrays it can hit the engine's argument-count limit (call stack size exceeded in V8 around ~125k args). The loop has no such ceiling.
Trace flatten([1, [2, [3]]]). There are three nested arrays, so the recursion goes three levels deep.
Step by step:
flatten([1, [2, [3]]]). i=0: arr[0] is 1, not an array — push 1. result is now [1]. i=1: arr[1] is [2, [3]], an array — recurse.flatten([2, [3]]). i=0: push 2. result here is [2]. i=1: arr[1] is [3], an array — recurse.flatten([3]). i=0: push 3. Loop ends. Return [3].inner = [3]. Push each element: result becomes [2, 3]. Loop ends. Return [2, 3].inner = [2, 3]. Push each: result becomes [1, 2, 3]. Loop ends. Return [1, 2, 3].Final answer: [1, 2, 3]. The deepest call returns first; each parent splices the result in and continues.
Complexity. Time is O(n) where n is the total number of values (leaves + intermediate array nodes); each is visited once. Space is O(d + n) — d for the call stack at the deepest point, plus the n-element output array. Truly pathological inputs (an array nested 50,000 levels deep) can blow the call stack; see Going further.
typeof value === 'object' instead of Array.isArray. typeof null === 'object', typeof {a: 1} === 'object', and typeof new Date() === 'object' all return true — your function would try to iterate null and crash, or unwrap an object's properties into the output. Array.isArray([1,2]) is true and is false for everything else, including array-likes like {0: 'a', length: 1}. Use it.result.push(...inner). Looks clean and reads well, but if inner has ~125k+ elements V8 throws RangeError: Maximum call stack size exceeded because each spread element becomes a separate function argument. The plain for (let j…) result.push(inner[j]) loop has no such limit.arr.flat() returns a new array; so should flatten. If you start with result = arr and push into it, you've corrupted the caller's data. Always start with const result = [].[1, , 3].length is 3, but index 1 is missing (not undefined). for..of and forEach skip holes; an indexed for loop does NOT — arr[1] returns undefined, which you'd then push, producing [1, undefined, 3]. The native flat skips holes, so we use i in arr to match.'abc' is iterable in JavaScript (for..of yields 'a','b','c'), but Array.isArray('abc') is false, so our guard correctly leaves strings as single elements. If you mistakenly check value[Symbol.iterator] or typeof value.length === 'number', you'll start splitting strings into characters and breaking objects with a length property.Real-world flatten implementations layer on a few extra capabilities:
flatten(arr, depth = Infinity) matches Array.prototype.flat(depth) — at each recursion you decrement and stop unwrapping when depth === 0. About 3 extra lines.flatMap companion. flatMap(fn) is arr.map(fn).flatten() but spec'd to flatten only one level. It's the building block behind several functional patterns (returning [] to skip an element, returning [a, b] to expand one) and falls out almost for free once you have flatten.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
You're given an array that may contain nested arrays, nested any number of levels deep. Implement flatten(arr) so it returns a new array with all nesting removed — every value pulled up to a single, flat level. The native version is Array.prototype.flat(Infinity); your job is to write the recursion yourself.
// Returns a new array with every nested array fully unwrapped.
// Does NOT mutate the input.
function flatten(arr: any[]): any[];
flatten([1, 2, 3]); // [1, 2, 3]
flatten([1, [2, 3]]); // [1, 2, 3]
flatten([1, [2, [3, [4]]]]); // [1, 2, 3, 4]
flatten([[[[[5]]]]]); // [5]
flatten([]); // []
flatten([1, [], [2, [3, []]]]); // [1, 2, 3]
flatten([1, 'a', [true, [null]]]); // [1, 'a', true, null]
Array.prototype.flat(Infinity) — there is no depth argument here, you go all the way down.[1, , 3]) are dropped, the same way the native flat drops them. Explicit undefined ([1, undefined, 3]) is kept.Array.isArray to decide. Objects, strings, typed arrays, and array-likes ({ 0: 'a', length: 1 }) stay as-is.arr.flat(). The whole point is to write the recursion. flat, flatMap, and JSON.parse(JSON.stringify(...)) are off-limits.You'll write a function that walks a nested array and pulls every value up to a single, flat level — the recursive twin of Array.prototype.flat(Infinity).
You're handed an array. Most of its slots hold values, but some slots hold more arrays — and those inner arrays can hold more arrays inside them, with no fixed limit on depth. Your job is to produce a new array that contains the same leaf values, in the same left-to-right order, but with all the wrapping arrays gone. Native Array.prototype.flat does this when you pass Infinity; here you implement the recursion by hand.
Picture the input as a tree. Numbers and strings are leaves; arrays are branches. flatten walks the tree depth-first, left to right, and collects every leaf it sees into one flat row. The branch structure is thrown away; only the leaves survive.
The fact that arrays-inside-arrays-inside-arrays is the same shape as just one array-inside-an-array is the cue that recursion fits: the problem of "flatten this" is solved by solving "flatten this smaller thing" and gluing the answer in.
If you've only seen one level of nesting, you might write the version that handles exactly that — peel off the outer wrapper, push each inner element in:
function flattenOneLevel(arr) {
const result = [];
for (const value of arr) {
if (Array.isArray(value)) {
for (const inner of value) result.push(inner);
} else {
result.push(value);
}
}
return result;
}
For [1, [2, 3], 4] this returns [1, 2, 3, 4] — looks right. Try [1, [2, [3, [4]]]] and you get [1, 2, [3, [4]]]. The function lifts the first layer of nesting but drops anything below as-is. We need to keep peeling until there's nothing left to peel.
The fix is to apply flatten to the inner array too — and let its recursion handle whatever depth is in there.
function flatten(arr) {
const result = [];
for (let i = 0; i < arr.length; i++) {
// Skip sparse holes. `i in arr` is false for [1, , 3] at i=1
// but TRUE for [1, undefined, 3] — matches Array.prototype.flat's behavior.
if (!(i in arr)) continue;
const value = arr[i];
if (Array.isArray(value)) {
// Recurse: trust that flatten(value) returns a fully flat array,
// then push each of its elements in. Don't push the inner array itself.
const inner = flatten(value);
for (let j = 0; j < inner.length; j++) {
result.push(inner[j]);
}
} else {
// Leaf value (number, string, object, etc.) — keep as-is.
result.push(value);
}
}
return result;
}
module.exports = { flatten };
Three things separate this from the naive version. First, Array.isArray(value) followed by flatten(value) instead of an inner for loop — that recursive call is the entire difference. Second, an indexed for loop plus i in arr to handle sparse holes — for..of and forEach walk holes inconsistently across engines, so we hand-roll the check. Third, result.push(inner[j]) one-by-one rather than result.push(...inner) — spreading works, but on very large arrays it can hit the engine's argument-count limit (call stack size exceeded in V8 around ~125k args). The loop has no such ceiling.
Trace flatten([1, [2, [3]]]). There are three nested arrays, so the recursion goes three levels deep.
Step by step:
flatten([1, [2, [3]]]). i=0: arr[0] is 1, not an array — push 1. result is now [1]. i=1: arr[1] is [2, [3]], an array — recurse.flatten([2, [3]]). i=0: push 2. result here is [2]. i=1: arr[1] is [3], an array — recurse.flatten([3]). i=0: push 3. Loop ends. Return [3].inner = [3]. Push each element: result becomes [2, 3]. Loop ends. Return [2, 3].inner = [2, 3]. Push each: result becomes [1, 2, 3]. Loop ends. Return [1, 2, 3].Final answer: [1, 2, 3]. The deepest call returns first; each parent splices the result in and continues.
Complexity. Time is O(n) where n is the total number of values (leaves + intermediate array nodes); each is visited once. Space is O(d + n) — d for the call stack at the deepest point, plus the n-element output array. Truly pathological inputs (an array nested 50,000 levels deep) can blow the call stack; see Going further.
typeof value === 'object' instead of Array.isArray. typeof null === 'object', typeof {a: 1} === 'object', and typeof new Date() === 'object' all return true — your function would try to iterate null and crash, or unwrap an object's properties into the output. Array.isArray([1,2]) is true and is false for everything else, including array-likes like {0: 'a', length: 1}. Use it.result.push(...inner). Looks clean and reads well, but if inner has ~125k+ elements V8 throws RangeError: Maximum call stack size exceeded because each spread element becomes a separate function argument. The plain for (let j…) result.push(inner[j]) loop has no such limit.arr.flat() returns a new array; so should flatten. If you start with result = arr and push into it, you've corrupted the caller's data. Always start with const result = [].[1, , 3].length is 3, but index 1 is missing (not undefined). for..of and forEach skip holes; an indexed for loop does NOT — arr[1] returns undefined, which you'd then push, producing [1, undefined, 3]. The native flat skips holes, so we use i in arr to match.'abc' is iterable in JavaScript (for..of yields 'a','b','c'), but Array.isArray('abc') is false, so our guard correctly leaves strings as single elements. If you mistakenly check value[Symbol.iterator] or typeof value.length === 'number', you'll start splitting strings into characters and breaking objects with a length property.Real-world flatten implementations layer on a few extra capabilities:
flatten(arr, depth = Infinity) matches Array.prototype.flat(depth) — at each recursion you decrement and stop unwrapping when depth === 0. About 3 extra lines.flatMap companion. flatMap(fn) is arr.map(fn).flatten() but spec'd to flatten only one level. It's the building block behind several functional patterns (returning [] to skip an element, returning [a, b] to expand one) and falls out almost for free once you have flatten.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.