类型级递归
使用递归条件类型遍历类型。
类型级递归 是 CoddyKit 上的免费 TypeScript Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 TypeScript Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 TypeScript Academy 课程共包含 4 节课。
类型中的递归
条件类型可以引用自身,从而让类型语言拥有循环。大多数类型级递归会逐个元素遍历元组,拆下头部,再对尾部递归。
type Length<T extends unknown[]> =
T extends [unknown, ...infer Rest]
? Length<Rest>
: 0;
// (this counts down to a base case)基本情况
每次递归都需要一个停止条件。对于元组来说,通常就是空元组。当模式 [head, ...rest] 不再匹配时,就表示已经到达末尾,此时返回一个固定结果。
type IsEmpty<T extends unknown[]> =
T extends [] ? true : false;
type A = IsEmpty<[]>; // true
type B = IsEmpty<[1, 2]>; // false头部和尾部
核心操作是使用带有展开模式的 infer,将元组拆分为第一个元素和剩余的元组。
type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never;
type Tail<T extends unknown[]> = T extends [unknown, ...infer R] ? R : [];
type A = Head<[1, 2, 3]>; // 1
type B = Tail<[1, 2, 3]>; // [2, 3]逐个处理元素
将头部、尾部和递归结合起来,就可以转换整个元组。ToStrings 会重新构建元组,将每个元素类型替换为 string。
type ToStrings<T extends unknown[]> =
T extends [infer H, ...infer R]
? [string, ...ToStrings<R>]
: [];
type A = ToStrings<[1, true, 3]>; // [string, string, string]累加器模式
您通常会在一个名为累加器的额外参数中逐步构建结果。它从空值开始,在每一步中不断增长。这样可以避免重复遍历结构,是类型级循环的标准技巧。
type Reverse<T extends unknown[], Acc extends unknown[] = []> =
T extends [infer H, ...infer R]
? Reverse<R, [H, ...Acc]>
: Acc;
type A = Reverse<[1, 2, 3]>; // [3, 2, 1]遍历累加器
跟踪 Reverse<[1,2,3]>:
- 步骤 1:头部为 1,累加器变为 [1]
- 步骤 2:头部为 2,累加器变为 [2,1]
- 步骤 3:头部为 3,累加器变为 [3,2,1]
- 元组为空,返回累加器 = [3,2,1]
累加器会在每次递归调用中传递答案。
type R = Reverse<["a", "b"]>; // ["b", "a"]对联合类型递归
您也可以通过递归将字符串联合起来。在这里,我们将元组元素连接成一个字符串字面量类型,并使用分隔符分开。
表示法:实际的 TypeScript 会使用由反引号分隔、包含美元符号大括号占位符的字符串来书写模板字面量类型。在这些代码片段中,我们将这种模式表示为 Tpl<...>,按顺序列出各个部分;例如,与字面量前缀和随后内容 Rest 匹配的反引号模板会表示为 Tpl<'prefix', infer Rest>。
type Join<T extends string[], Sep extends string = ','> =
T extends [infer H extends string, ...infer R extends string[]]
? R extends []
? H
: Tpl<H, Sep, Join<R, Sep>>
: '';
// Tpl<H, Sep, ...> builds a template literal type joining the parts
type A = Join<['a', 'b', 'c']>; // 'a,b,c'使用递归计数
递归加上累加器就可以进行计数。构建一个与输入长度相同、由 unknown 组成的元组,然后读取它的 length。您将在算术课程中大量使用这一技巧。
type Count<T extends unknown[], Acc extends unknown[] = []> =
T extends [unknown, ...infer R]
? Count<R, [unknown, ...Acc]>
: Acc["length"];
type A = Count<["x", "y", "z"]>; // 3使用递归筛选
遍历元组,只保留符合条件的元素,丢弃其他元素。只要不将某个元素加入累加器,就可以跳过它。
type KeepStrings<T extends unknown[], Acc extends unknown[] = []> =
T extends [infer H, ...infer R]
? H extends string
? KeepStrings<R, [...Acc, H]>
: KeepStrings<R, Acc>
: Acc;
type A = KeepStrings<[1, "a", 2, "b"]>; // ["a", "b"]递归深度限制
编译器会限制递归深度(历史上大约为 50,但尾递归优化可以让某些模式达到更高深度)。对于非常大的元组,您可能会遇到类型实例化过深的问题。请使用累加器(尾递归)风格来提高这个上限。
type Repeat<T, N extends number, Acc extends T[] = []> =
Acc["length"] extends N ? Acc : Repeat<T, N, [...Acc, T]>;
type A = Repeat<0, 3>; // [0, 0, 0]整合运用
头部与尾部拆分、基本情况和累加器,是几乎所有类型级循环的三个组成部分。借助它们,您可以完全在类型系统中映射、筛选、反转、计数和连接元组。
type MapToPairs<T extends unknown[], Acc extends unknown[] = []> =
T extends [infer H, ...infer R]
? MapToPairs<R, [...Acc, [H, H]]>
: Acc;
type A = MapToPairs<[1, 2]>; // [[1, 1], [2, 2]]快速检查
测试您对累加器模式的理解。
回顾
现在,您已经可以在类型级别编写循环。
- 自引用条件类型会创建递归。
- 将元组拆分为
[H, ...R],然后对尾部递归。 - 基本情况(空元组)会停止循环。
- 累加器会构建结果,并支持更深层的递归。
接下来:条件类型在联合类型上的特殊行为。
常见问题解答
「类型级递归」课时是免费的吗?
是的 — 「类型级递归」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 TypeScript Academy 课程的其余内容,请升级到 CoddyKit PRO。 TypeScript Academy 课程共包含 4 节课。
「类型级递归」这节课中我会学到什么?
使用递归条件类型遍历类型。 你通过在浏览器中直接运行的动手代码来练习 TypeScript Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 TypeScript Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 TypeScript Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「类型级递归」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 TypeScript Academy 课中编写并运行代码吗?
能。每节 TypeScript Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。