TypeScript 递归类型怎么写
你写 TypeScript 代码时,是不是遇到过这种情况:一个接口里的属性,又需要是另一个相同结构的接口,而且套了好几层?比如做一个无限级的分类菜单,或者嵌套的评论回复,要是用普通的类型定义,要么重复写一堆结构,要么一不小心就写崩了。这时候“递归类型”就能帮你省事儿。
什么是递归类型?
简单说,递归就是“自己调用自己”。你可以把它理解成“套娃”——一个东西的定义里,包含了和它本身一样的结构。比如你拆俄罗斯套娃,外面是一个大娃娃,打开里面是个小一号的套娃,再打开又是更小的,直到最里面的实心娃娃。放到类型里,就是一个类型的属性,本身也是这个类型。

举个生活化的例子:你手机里的相册,能建相册,相册里还能建相册,最后才是照片。用递归类型就能把这种“无限嵌套相册”的结构定下来,不用提前说死“最多3层”,也不用重复写好几段类似的结构。
怎么写最基础的递归类型?
先从简单的来,用一个“分类菜单”的场景试试。假设我们要做一个可以无限子分类的菜单,不用递归的话,得写死子分类的层级:
`typescript
// 不推荐:提前写死层级,太啰嗦
interface MenuItem {
id: number;
name: string;
children: MenuItem[]; // 这里就是递归的关键!
}
`
是不是看到了?MenuItem里的children属性,类型是MenuItem的数组——这不就是“自己包含自己”吗?这就是最基础的递归类型。现在你可以随便嵌套了:
`typescript
const menu: MenuItem = {
id: 1,
name: "电子产品",
children: [
{
id: 2,
name: "手机",
children: [
{ id: 3, name: "智能手机", children: [] },
{ id: 4, name: "功能机", children: [] }
]
},
{
id: 5,
name: "电脑",
children: []
}
]
};
`
TypeScript 完全能识别这种结构,不会报错,而且你点menu的属性时,还能自动提示id、name、children,不用自己记,这就是类型的好处。
要注意的“边界”问题
递归类型虽然方便,但有个容易踩的坑:如果没有“终止条件”,就会无限套娃,甚至导致代码崩溃。就像你拆套娃时,要是没个实心的最内层,拆到最后啥都没有,肯定不行。
刚才的MenuItem里,children是MenuItem[],最内层的分类,children是空数组就行,这就是“终止”。要是你不小心把children写成MenuItem而不是MenuItem[],还忘了给最内层留终止,TypeScript 就会报“类型深度过深”的错,甚至编译失败。
复杂场景的递归类型怎么写?
如果你的递归类型需要带一些额外属性,比如评论系统里,每条评论不仅有子评论,还有“点赞数”“发布时间”,也可以用泛型来优化,让类型更灵活。比如:
`typescript
interface CommentNode<T> {
content: string;
likeCount: number;
replies: CommentNode<T>[]; // 还是自己递归,只是外层加了泛型
}
`
这样不管是普通评论还是带图片的评论,都能复用这个结构。比如带图片的评论:
`typescript
interface ImageComment {
imageUrl: string;
}
const comment: CommentNode<ImageComment> = {
content: "这张照片拍得好!",
likeCount: 10,
imageUrl: "xxx.jpg",
replies: []
};
`
这种写法比直接写死Comment更灵活,适合需要复用结构的场景。
其实TypeScript的递归类型没那么难,核心就是抓住“自己引用自己”和“终止条件”这两个关键点。下次遇到嵌套结构时,别再重复写类型了,试试用递归类型,代码会清爽很多。