Yuzhe's Blog

yuzhes

0459 · Flatten

0459 · Flatten

Challenge Link

Problem

Implement a type Flatten<T> that takes a nested array or tuple type and produces a flattened version.

type A = Flatten<[1, 2, [3, 4], [[[5]]]]>
// [1, 2, 3, 4, 5]

This challenge is about recursively walking through a tuple, checking each element, and expanding nested arrays into the final result.

Solution

type Flatten<T extends readonly unknown[]> = T extends readonly [
  infer First,
  ...infer Rest,
]
  ? First extends readonly unknown[]
    ? [...Flatten<First>, ...Flatten<Rest>]
    : [First, ...Flatten<Rest>]
  : []

Explanation

The core idea is:

Step by Step

  1. T extends readonly unknown[] constrains the input to arrays and tuples, including readonly tuples.
  2. T extends readonly [infer First, ...infer Rest] destructures the tuple into its first element and the remaining elements.
  3. If First extends readonly unknown[], then First is another nested array, so flatten it and spread the result.
  4. Otherwise, keep First as a single element.
  5. Recurse on Rest.
  6. When T is empty, return [].

Why Use readonly unknown[]?

Many type-challenge test cases use readonly tuples inferred from as const.

const data = [1, [2, 3]] as const

The type of data is readonly, so Flatten<T extends readonly unknown[]> is more flexible than Flatten<T extends unknown[]>.

The readonly marker also needs to appear in the tuple pattern. Otherwise a readonly tuple may fail to match the recursive branch.

Example Walkthrough

Start with:

type Result = Flatten<[1, [2, [3]], 4]>

The recursion unfolds like this:

Flatten<[1, [2, [3]], 4]>
-> [1, ...Flatten<[[2, [3]], 4]>]
-> [1, ...Flatten<[2, [3]]>, ...Flatten<[4]>]
-> [1, 2, ...Flatten<[[3]]>, 4]
-> [1, 2, ...Flatten<[3]>, 4]
-> [1, 2, 3, 4]

Each nested array is flattened before being merged back into the surrounding result.

Alternative Solutions

Option 1: Accumulator Style

type Flatten2<
  T extends readonly unknown[],
  Acc extends readonly unknown[] = [],
> = T extends readonly [infer First, ...infer Rest]
  ? First extends readonly unknown[]
    ? Flatten2<Rest, [...Acc, ...Flatten2<First>]>
    : Flatten2<Rest, [...Acc, First]>
  : Acc

This version collects the result in Acc. It can be useful if you prefer an explicit “build the answer as you go” style.

Option 2: Mutable Array Constraint

type Flatten3<T extends unknown[]> = T extends [infer First, ...infer Rest]
  ? First extends unknown[]
    ? [...Flatten3<First>, ...Flatten3<Rest>]
    : [First, ...Flatten3<Rest>]
  : []

This is shorter, but it is less compatible because it does not accept readonly tuples.

Thought Process

A flatten operation is naturally recursive:

That maps directly onto tuple recursion with [infer First, ...infer Rest].

The important insight is that we are solving two problems at the same time:

  1. walking across the top-level tuple
  2. diving into nested tuples when an element is itself an array

The spread syntax in the result lets both parts compose nicely.

Key Takeaways

Key concepts:

中文解析

类型定义解读

type Flatten<T extends readonly unknown[]> =
  T extends readonly [infer First, ...infer Rest]  // 拆出头元素 First 与剩余 Rest
    ? First extends readonly unknown[]             // 判断 First 是否还是数组/元组
      ? [...Flatten<First>, ...Flatten<Rest>]      // 是数组 → 递归展平 First,再接上展平后的 Rest
      : [First, ...Flatten<Rest>]                  // 不是数组 → 保留 First,继续处理 Rest
    : []                                           // T 为空元组 → 返回 []

逐步分析

整体思路:头尾递归 + 双重展开

对于每个元素:

  1. 如果它是数组 → 先展平它(递归),再把结果拼进去
  2. 如果它不是数组 → 直接保留,继续处理剩余

两个子问题在展开时自然合并:[...flatFirst, ...flatRest]

递归展开示例

Flatten<[1, [2, [3]], 4]>

步骤1: First=1, Rest=[[2,[3]], 4]
  1 不是数组 → [1, ...Flatten<[[2,[3]], 4]>]

步骤2: First=[2,[3]], Rest=[4]
  [2,[3]] 是数组 → [...Flatten<[2,[3]]>, ...Flatten<[4]>]

步骤3: Flatten<[2,[3]]>
  First=2, Rest=[[3]]
  2 不是数组 → [2, ...Flatten<[[3]]>]
  First=[3], Rest=[]
  [3] 是数组 → [...Flatten<[3]>, ...Flatten<[]>]
  = [...[3], ...[]] = [3]
  → [2, 3]

步骤4: Flatten<[4]> = [4]

最终: [1, 2, 3, 4] ✅

为什么两处都要写 readonly

如果只在约束处写 readonly,但条件分支里的模式不带 readonly,那么 readonly [1, 2] 就无法匹配该分支,导致直接返回 []

考察知识点