当 TypeScript 类型嵌套超过了 100 层

在项目中执行 tsc --noemit 时,遇到了 Excessive stack depth comparing types 错误。本文将记录我解决此问题的过程,包括分析错误原因、排查过程以及最终的解决方案。

错误定位

1. 错误信息

首先,通过执行 tsc --noemit,终端出现如下错误信息:

text 复制代码
Excessive stack depth comparing types

这表明 TypeScript 的类型检查在比较类型时,栈深度超出了最大限制。

2. 确认 tsc --noemit 执行命令

通过查看 package.json 配置,确认项目的 TypeScript 编译命令为:

arduino 复制代码
"tsc --noemit"

此命令会执行 TypeScript 编译器,但不生成输出文件,仅进行类型检查。

3. 进入 TypeScript 源码进行分析

node_modules/typescript/lib/tsc.js 中搜索关键字 Excessive stack depth comparing types,找到对应的 key

text 复制代码
Excessive_stack_depth_comparing_types_0_and_1

继续搜索 Excessive_stack_depth_comparing_types_0_and_1,发现该错误在 relationCount > 0 时被抛出。

4. 查找 overflow = true 的赋值位置

搜索 overflow = true,发现两个相关代码位置,分别添加调试日志:

javascript 复制代码
console.log(">>> relationCount -> true", relationCount);
javascript 复制代码
console.log(

">>> overflow --> true [sourceDepth || targetDepth]",

sourceDepth,

targetDepth

);

console.log("=== TYPE DEPTH LIMIT REACHED ===");

console.log("Source type:", typeToString(source));

console.log("Target type:", typeToString(target));

console.log(

"Source stack:",

sourceStack.map((t) => typeToString(t))

);

console.log(

"Target stack:",

targetStack.map((t) => typeToString(t))

);

5. 观察 tsc --noemit 的控制台输出

通过观察 tsc --noemit 输出的日志,发现 Source stack 中包含了大量的类型信息,尤其是新增的类型定义。通过调试日志,我们得到了更详细的类型堆栈信息。

错误原因分析

经分析,问题的根本原因在于 类型嵌套 的加深。项目的数据模型发生了变更,导致类型之间的关联性增多。codegen 生成的代码增加了大量的类型嵌套,特别是像 Maybe<T> 这样的联合类型,进一步加剧了类型检查的深度,最终导致 TypeScript 超过了 sourceDepth 的最大限制(默认为 100),触发了错误。

解决方案

1. 增加调试日志,查看最大深度

tsc 代码中手动放开 sourceDepthtargetDepth 限制,并添加日志:

javascript 复制代码
if (sourceDepth >= 100 || targetDepth >= 100) {

console.log(

`深度检查点: sourceDepth=${sourceDepth}, targetDepth=${targetDepth}`

);

}

执行后输出:

ini 复制代码
深度检查点: sourceDepth=105, targetDepth=104

可以看到,sourceDepth 超过了 100,触发了错误。

2. 分析 Source stack

通过查看 Source stack 中的类型信息,发现有许多类型是重复的。例如:

arduino 复制代码
"WhereInput | null","WhereInput"

怀疑是 Maybe 类型 (T | null | undefined) 导致了额外的类型检查,这些重复的类型增加了堆栈的深度。

3. 修改 codegen 配置,优化类型定义

codegen 配置文件中调整 maybeValue 配置:

yaml 复制代码
maybeValue: T | undefined

原先生成的类型:

typescript 复制代码
export type Maybe<T> = T | null | undefined;

修改后再次运行 tscSource stack 数量下降约 50%,但 Target stack 仍然较大。

4. 深入分析 TypeScript 类型检查策略

TypeScript 在检查 Maybe<WhereInput> (即 WhereInput | null | undefined) 类型时,会拆分联合类型,并逐个成员检查兼容性,例如:

  1. WhereInput | null 是否兼容 SerializableObject

  2. WhereInput 是否兼容 SerializableObject

优化 Maybe 类型后,减少了部分嵌套检查,最终 tsc 执行成功。

优化后的要点

  1. 类型嵌套过深:Maybe 类型的嵌套会导致类型检查时栈深度过大,进而引发 Excessive stack depth comparing types 错误。

  2. 修改 codegen 配置:通过调整 Maybe 类型的定义(T | null | undefined → T | undefined),有效减少了类型堆栈的深度。

  3. 深入分析 TypeScript 类型检查:理解 TypeScript 在检查联合类型时的机制,避免不必要的类型拆解,优化了类型兼容性检查的过程。

使用 TypeScript 过程中,尽量不用或者减少使用类型自动推断

自动推断类型在 TypeScript 中是一个非常强大的特性,它能减少显式类型声明的工作量,但在某些情况下,过度依赖类型推断可能会导致意料之外的结果,特别是在复杂的类型结构和大规模的代码库中。过度依赖自动推断可能导致以下问题:

类型堆栈过深

类型推断过程中,尤其是复杂的联合类型或递归类型,可能会生成嵌套过深的类型结构。例如,Maybe 类型可能会被自动推断为 T | null | undefined,这会增加类型检查的深度。

推断不准确

TypeScript 的类型推断并不总是能准确捕捉到开发者的意图。在某些场景下,自动推断的类型可能并非最佳选择,导致后续代码出现类型不匹配或不必要的类型转换。

性能问题

在类型检查过程中,TypeScript 需要推断并验证复杂的类型结构。过多的推断操作可能导致类型检查速度变慢,甚至触发栈溢出错误。

为了避免这些问题,建议使用以下方式避免类型自动推断:

  1. 使用显式类型声明

尽可能为复杂的类型,尤其是泛型和联合类型,提供明确的类型声明。显式声明类型能帮助 TypeScript 更精确地理解类型结构,并减少不必要的类型推断。例如,使用 Maybe 类型时,可以考虑明确声明为 T | undefined 而不是 T | null | undefined。

  1. 限制递归类型的使用

递归类型会导致类型推断的深度急剧增加。在使用递归类型时,要特别小心,避免无意中创建过深的类型层级。可以考虑将递归类型限制在较小的深度范围内,或使用更简单的类型结构来避免过深的嵌套。

  1. 显式推断类型

对于需要通过推断来确定类型的地方,可以使用 TypeScript 提供的 as 关键字明确指定类型,而不是让编译器推断。比如当你知道某个值的类型时,最好显式地声明它,而不是依赖 TypeScript 的推断。

  1. 避免冗余类型定义

对于复杂的联合类型或类型交集,尽量避免冗余定义。冗余的联合类型会增加类型检查的复杂度,从而导致栈深度过深。通过减少不必要的联合类型(如 T | null | undefined)来优化类型定义,能够有效减小类型检查的负担。

通过减少类型自动推断的过度使用,显式定义类型,能够使 TypeScript 更加高效地进行类型检查,避免因类型层级过深而引发 Excessive stack depth comparing types 错误。

结语

如果你在项目中遇到类似的 stack depth 错误,不妨回顾一下代码生成过程中的类型定义,并适当优化。尤其是使用像 Maybe 这类联合类型时,尽量避免不必要的类型嵌套,这将有效减少类型检查的深度,避免出现栈溢出的错误。

相关推荐
天蓝色的鱼鱼4 分钟前
都2026年了还不会Vite插件开发?手写一个版本管理插件,5分钟包会!
前端·vite
苏武难飞15 分钟前
分享一个33号远征队的效果!
前端
鹏程十八少37 分钟前
4.Android 30分钟手写一个简单版shadow, 从零理解shadow插件化零反射插件化原理
android·前端·面试
亿元程序员44 分钟前
这款值68亿的游戏,你不实战一下吗?安排!
前端
摸鱼的春哥1 小时前
Agent教程15:认识LangChain(中),状态机思维
前端·javascript·后端
明月_清风1 小时前
告别遮挡:用 scroll-padding 实现优雅的锚点跳转
前端·javascript
明月_清风1 小时前
原生 JS 侧边栏缩放:从 DOM 监听到底层优化
前端·javascript
万少10 小时前
HarmonyOS 开发必会 5 种 Builder 详解
前端·harmonyos
橙序员小站13 小时前
Agent Skill 是什么?一文讲透 Agent Skill 的设计与实现
前端·后端
炫饭第一名15 小时前
速通Canvas指北🦮——基础入门篇
前端·javascript·程序员