如何使用Qodana修复常见的TypeScript问题
[LOADING...]
大多数 TypeScript 项目已经使用 ESLint 配合 @typescript-eslint。这涵盖了很多内容:显式 any、未处理的 Promise、非空断言等等。如果你的 lint 配置足够完善,你可以在代码审查之前就在编辑器中捕获那些显而易见的问题。
ESLint 规则无法产生跨文件的检查结果。每条规则只在单个文件范围内运行,这意味着 ESLint 无法告诉你某个导出在整个代码库中未被使用、某个文件中 any 类型的值导致五个文件之外出现了不安全假设、或者两个组件独立实现了相同的逻辑。这就是 Qodana 所填补的空白。
以下是五个值得关注的 TypeScript 问题,按 ESLint 能处理的范围和超出其能力范围的情况进行组织。
隐式 any 在代码库中扩散
ESLint 的 no-explicit-any 规则能捕获你显式写出 any 的地方。但它无法追踪 any 从外部来源(例如 response.json()、无类型的第三方库或未类型化的导入)进入代码库后发生了什么。一旦外部类型的 any 值进入你的代码,它会通过属性访问和函数调用静默传播。ESLint 的 no-unsafe-* 规则可以捕获这一点,但前提是你使用了 @typescript-eslint/recommended-type-checked,这需要类型感知的 lint 功能,且远比标准推荐的配置罕见。
response.json() 在标准库中返回 any。下游所有内容都是无类型的。编译器接受任何属性名和任何方法调用。错误在运行时才会暴露。Qodana 会追踪 any 如何在程序中的文件之间流动。当 any 类型的值到达一个期望特定形状的代码路径时,Qodana 会标记出这种不匹配,即使这距离 any 进入代码库已有多个函数调用。
添加 UserResponse 并不能解决这个问题。它只是将谎言移到了崩溃点附近。正确的做法应该是为边界进行类型化:
如果 API 响应结构发生了变化,类型错误会在编译时暴露出来。
非空断言被用作捷径
ESLint 的 no-non-null-assertion 规则会统一标记所有的 ! 操作符。这很有效,但许多团队会禁用该规则或添加宽泛的例外情况,因为合法的使用场景(例如在运行时检查之后)会与危险的使用场景一同被标记。信号变得嘈杂,规则被关闭,问题也就从视野中消失了。
两个例子都能编译通过,没有错误。在可预测的条件下都会崩溃。! 通常是为了消除类型错误而添加的,但并未修复根本问题。
正确的做法是处理 null 的情况:
Qodana 将非空断言作为报告中的一个单独类别来呈现。并非每个 ! 都是错误的,但将它们集中在一起查看,可以更容易地分辨出合法用途和捷径,而不必在嘈杂的规则和完全没有规则之间做选择。
未处理的 Promise
ESLint 的 @typescript-eslint/no-floating-promises 规则很有效,但它是一条类型感知规则。它需要在 ESLint 配置中通过 parserOptions.project 启用 TypeScript 类型检查。在未配置此选项或仅配置了部分代码库的项目中,该规则会在未覆盖的文件上静默地不起作用。
TypeScript 会静默接受这段代码。调用 async 函数但不使用 await 被认为是有效语法,返回值会被丢弃。然而,这种行为是错误的:用户在保存完成之前就看到了成功页面,并且任何数据库错误都会被静默吞掉。
Qodana 的分析默认在整个项目范围内都是类型感知的,无需单独配置 ESLint 的 TypeScript 集成。无论项目的 ESLint 设置结构如何,未处理的 Promise 都会被一致地标记出来。
未使用的导出
tsconfig 中的 noUnusedLocals 能捕获文件内未使用的变量。但导出的符号被有意排除在外。从编译器的角度来看,当前文件之外的某些内容可能会导入它们。ESLint 的 eslint-plugin-import 提供了 import/no-unused-modules 规则来检测这一点,但这需要在每次 lint 运行时扫描整个依赖图,并且在大型代码库中会带来显著性能开销。对于大多数项目来说,保持该规则开启并不现实。
三个导出都通过检查而没有警告。但 formatPercent 和 formatBytes 是死代码。它们增加了维护面,拖慢了重构速度,并且会误导开发人员以为导出的符号正在被使用。
检测这一点需要全项目分析。Qodana 在整个代码库中构建引用图,追踪每个导入和重新导出。仅作为源出现、从未作为导入目标的符号会被标记出来。无论是 tsc 还是 ESLint 都无法做到这一点。
跨文件的重复逻辑
ESLint 本身没有重复代码检测功能。存在像 jscpd 这样的独立工具,但它们不属于你的 lint 流水线。这意味着需要单独的设置、单独的维护,以及另一件需要记住的事情。结果是:在组件或工具文件之间复制的逻辑不断累积,却无人标记。
这不是一个风格问题。它意味着错误修复需要在多个地方应用,而当它们没有被应用时,两个副本之间的行为会静默地产生分歧。
Qodana 在同一个分析过程中检测跨文件的重复代码,该过程也会暴露类型问题和未使用的导出。当它与其他所有问题一起出现在报告中时,它比一个没人记得运行的独立工具更难被优先级排后。
为你的 TypeScript 项目设置 Qodana
以上五个问题都可以通过 Qodana 针对 JavaScript 和 TypeScript 项目的默认配置文件可见。以下是一个最小的 qodana.yaml 文件,可帮助你入门:
如果第一次运行报告了数百个现有问题,请不要因此阻止 CI 采纳。Qodana 的基线功能会将项目的当前状态捕获到一个 qodana.sarif.json 文件中。提交该文件后,从那时起,CI 只会因新引入的问题而失败。现有的遗留问题仍然在报告中可见,但在你逐步处理它们的过程中,不会阻塞每一次 PR。
[LOADING...]
准备好用 Qodana 修复常见的 TypeScript 问题了吗?
试用 Qodana,并告诉我们你的想法。
我们特别感谢 Qodana 开发者 Lev Liadov 对本指南的贡献。