TypeScript 递归类型怎么写

你写 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的递归类型没那么难,核心就是抓住“自己引用自己”和“终止条件”这两个关键点。下次遇到嵌套结构时,别再重复写类型了,试试用递归类型,代码会清爽很多。

上一篇 JavaScript 如何做错误上报