TypeScript 如何做递归类型
很多朋友刚接触 TypeScript 时,会发现它比 JavaScript 多了很多“约束规则”,这些规则看似麻烦,却能帮我们提前揪出代码里的小错误——就像给代码加了一本“字典”,告诉编译器什么样的数据是合法的。而递归类型,就是这本字典里能“举一反三”的特殊规则,今天咱们就用生活化的例子把它说透。
什么是递归类型?
先想个日常场景:你整理衣柜,里面叠着衣服,最外层是一件外套,掀开外套里是一件毛衣,毛衣里面藏着一件短袖,这就是“套娃”一样的层级结构。递归类型就是用来定义这种“自己套自己”的结构的。

用 JavaScript 写这种结构很简单,比如一个无限嵌套的对象:
`javascript
const nestedData = {
value: 10,
next: {
value: 20,
next: {
value: 30,
next: null
}
}
};
`
但如果用 JavaScript 写函数处理它,很容易把属性写错,比如把value写成val,编译器根本不会提醒。这时候 TypeScript 的递归类型就能派上用场——它能自动识别这种嵌套结构的每一层,保证每一层的属性都符合规则。
怎么用 TypeScript 写递归类型?
写递归类型的核心,就是在类型定义里“引用自己”,就像套娃里的“娃娃”本身还是“套娃”。咱们还是用刚才的嵌套对象举例:
`typescript
// 定义一个递归类型
type NestedNumber = {
value: number;
// 这里引用自己,next可以是NestedNumber或者null(到最底层就断了)
next: NestedNumber | null;
};
// 用这个类型定义变量,编译器就会自动检查每一层
const validData: NestedNumber = {
value: 10,
next: {
value: 20,
next: {
value: 30,
next: null
}
}
};
`
这时候如果把next里的value改成字符串,比如value: "20",TypeScript 会立刻报错:“这里应该是数字,你给了字符串”,比 JavaScript 安全多了。
递归类型的两种常见用法
递归类型不是只能用来套对象,还有两种特别实用的场景,咱们用例子说清楚。
第一种是处理数组的嵌套结构。比如你做一个多层级的商品分类:家电下面分电视、冰箱,电视下面又分OLED、LED,这时候可以用递归类型定义:
`typescript
// 分类的每一项,要么是字符串,要么是包含子分类的结构
type Category = string | { name: string; children: Category[] };
const phoneCategory: Category = {
name: "数码",
children: [
{
name: "手机",
children: ["苹果", "华为"]
},
"平板"
]
};
`
这个类型能保证children里不管是子分类还是字符串,都符合规范,不会出现格式混乱。
第二种是处理函数的嵌套。比如你要做一个能无限嵌套的计算函数,比如add(1)(2)(3),这时候递归类型可以这样写:
`typescript
type AddFunc = (num: number) => AddResult;
type AddResult = AddFunc | number;
// 实现这个函数
const add = (x: number): AddFunc => (y) => add(x + y);
// 最后取结果时加个valueOf就行,或者自己包装一下
console.log(add(1)(2)(3).valueOf()); // 输出6
`
这里的AddFunc引用了自己,保证每次调用返回的都是符合要求的函数,不会变成别的类型。
写递归类型要注意的小坑
递归类型虽然好用,但有个关键细节不能忘:必须有“终止条件”,也就是刚才例子里的next: null或者number,不然会变成无限循环,编译器会报错。比如如果刚才的NestedNumber里没有null,就会一直套自己,永远定义不完。
另外,递归类型不能随便乱用。如果你的数据最多只有1层嵌套,完全没必要写递归类型,直接写{ value: number; next: number }就行,不然会让类型变得复杂,反而增加代码的理解成本。
总的来说,TypeScript的递归类型就是给“套娃数据”做了一本精准的说明书,让编译器帮你盯着每一层的格式,不用等到运行时才发现错误。掌握了这个技巧,你就能轻松应对那些多层嵌套的复杂数据结构了。