Flatten
Flatten
Challenge
In this challenge, you would need to write a type that takes an array and emits the flatten array type.
type flatten = Flatten<[1, 2, [3, 4], [[[5]]]]>
// [1, 2, 3, 4, 5]Solution
type Flatten<T extends unknown[]> = T extends [infer First, ...infer Rest]
? First extends unknown[]
? [...Flatten<First>, ...Flatten<Rest>]
: [First, ...Flatten<Rest>]
: []The trick is recursive tuple unpacking: peel the first element off the tuple, check if it's an array, and either flatten it further or keep it as-is.
Breaking it down
Step 1 — Base case: empty tuple
T extends [infer First, ...infer Rest]If T is an empty array [], it doesn't extend [infer First, ...infer Rest], so we fall to the : [] branch — returning an empty tuple. Recursion stops here.
Step 2 — Split head and tail
For T = [1, 2, [3, 4], [[[5]]]]:
First=1Rest=[2, [3, 4], [[[5]]]]
We then check what First is.
Step 3 — Is First itself an array?
First extends unknown[]
? [...Flatten<First>, ...Flatten<Rest>]
: [First, ...Flatten<Rest>]- If
Firstis an array → recursively flatten it and spread into the result - If
Firstis a plain value → wrap it in[First]and recurse onRest
Full trace for [1, [2, [3]]]:
Flatten<[1, [2, [3]]]>
First=1 (not array) → [1, ...Flatten<[[2, [3]]]>]
First=[2,[3]] (array) → [...Flatten<[2,[3]]>, ...Flatten<[]>]
First=2 (not array) → [2, ...Flatten<[[3]]>]
First=[3] (array) → [...Flatten<[3]>, ...Flatten<[]>]
First=3 (not array) → [3, ...Flatten<[]>]
Flatten<[]> = []
→ [3]
→ [3]
→ [2, 3]
→ [1, 2, 3]Deep Dive
Why unknown[] not any[]?
Typing the constraint as T extends unknown[] is safer than any[]. unknown forces us to narrow before using the value, whereas any silently skips type checks. Here it makes no practical difference at runtime, but it communicates intent: "I don't know what's inside yet, I'll check."
The spread-infer pattern
T extends [infer Head, ...infer Tail] is the fundamental idiom for recursive tuple processing in TypeScript. It mirrors functional programming's head :: tail destructuring. You'll see it in:
Flatten(this challenge)Reverse,Last,Pop,Shift- Any recursive tuple walk
Mastering this pattern unlocks the rest of the tuple-manipulation challenges.
Depth: infinite recursion concern?
TypeScript limits recursion depth (around 1000 for tail calls, less for complex types). For deeply nested arrays like [[[[...]]]], you may eventually hit:
Type instantiation is excessively deep and possibly infinite.In practice, with normally nested data this won't trigger. If you need a depth-limited flatten, you can add an accumulator tuple as a depth counter — similar to how FlattenDepth (challenge 3243) works.
Alternative: flat approach using spread
Some solutions use a helper with an accumulator:
type Flatten<
T extends unknown[],
Acc extends unknown[] = []
> = T extends [infer Head, ...infer Tail]
? Head extends unknown[]
? Flatten<Tail, [...Acc, ...Flatten<Head>]>
: Flatten<Tail, [...Acc, Head]>
: AccThis collects results in Acc as it goes, which can be slightly more tail-recursive-friendly. Both approaches are valid.
Key Takeaways
[infer Head, ...infer Tail]is the standard pattern for recursive tuple destructuringFirst extends unknown[]lets us check if an element is itself an array before deciding how to handle it- Recursion terminates when
Tcan no longer match the[First, ...Rest]pattern (i.e., it's empty[]) - The same recursive spread pattern applies to many tuple challenges — learn it once, use it everywhere
