为什么类型检查需要 TypeScript

类型化的代码检查非常强大,但需要完整的类型检查器才能良好运行。目前,这意味着使用 TypeScript。这也是为什么我们还没有找到合适替代方案的原因。

什么是类型化 Linting?

带有类型信息的 linting,也称为“类型化 linting”或“类型感知 linting”,是指编写使用类型信息来理解代码的 lint 规则。由 typescript-eslint 提供的类型化 linting 是当前常用的 JavaScript/TypeScript 中最强大的 linting 方式。使用类型信息的 lint 规则比传统的仅基于 AST 的规则更为强大。

许多流行的 lint 规则要么依赖类型化 linting,要么在没有类型化 linting 时不得不处理已知的漏洞或功能差距。ESLint 的核心规则并不理解类型信息,这使得一些 typescript-eslint 的“扩展”规则增加了类型信息。

因此,类型化 linting 对于 lint 工具来说非常重要。

那么,如何获取类型信息呢?

TypeScript 提供类型信息

TypeScript 是为 JavaScript 或 TypeScript 代码提供完整类型信息的工具。在当今的 Web 开发时代,它无疑是最受欢迎的 JavaScript 变体。

事实上,TypeScript 是目前唯一能够合理地获取类型信息的工具。所有试图重新创建它的公开努力要么被放弃,要么停滞不前。Flow 明确表示不会与 TypeScript 竞争公众认知度。当前最接近的公开尝试是 Ezno,但还处于非常早期的阶段。

如果你希望为 JavaScript/TypeScript 项目使用完整的类型检查器(实际上你确实需要),那么 TypeScript 是目前唯一合理的选择。

替代方案与难题

TypeScript 是一个庞大的依赖项,类型检查是一个众所周知的耗时过程。TypeScript 以 JavaScript 速度执行(“JIT”,即时编译),因此比使用 Go 或 Rust 等本地语言编写的工具占用更多的内存和执行时间。

如果我们能找到一种方法,不必在类型化 linting 中运行完整的 JIT 速度类型检查器,那将对 lint 用户非常有利。遗憾的是,这样的壮举尚未成功实现。

仅基于 AST 的类型

避免 TypeScript 依赖的一种方法可能是仅支持有限的类型检索:即仅查看 AST 中可见的内容。通过在文件中进行基本的 AST 检查,你可能可以在许多函数上取得一些进展,并通过基本的 TypeScript 解析器进一步构建每个文件的作用域管理器,有效地查找标识符的声明位置。

遗憾的是,仅基于 AST 的类型查找系统在遇到任何复杂的 TypeScript 类型时很快就会崩溃(例如条件类型或映射类型)。大多数较大的 TypeScript 项目在某些地方都会使用复杂类型。任何现代 ORM(如 Prisma、Supabase)或架构验证库(如 Arktype、Zod)都会使用条件类型等技术。如果无法理解这些类型,规则就无法理解引用这些类型的代码。

不一致的类型感知水平对用户来说充其量是混乱的,在实践中则几乎阻碍了 linter 的真正采用。

完整的类型系统(如 TypeScript 的)是唯一能够编写真正有效的 lint 规则的途径,这些规则能够执行任何定义跳转或基于类型的逻辑。

原生速度的重新实现

如果类型检查如此重要,且原生速度的类型检查器如此有益,为什么还没有人用 Go 或 Rust 重新实现 TypeScript?

TypeScript 是一个由微软资助的经验丰富的团队以及活跃的社区开发的庞大项目。每三个月发布一个新版本,增加类型检查的漏洞修复和功能。跟上 TypeScript 的发展对于任何团队来说都是巨大的挑战。

我希望有一天会有一个工具可以与 TypeScript 竞争。但这样的工具需要多年时间才能发展。

子集重实现

TypeScript 由多个功能领域组成,其中类型检查只是其中之一。类型化 linting 使用的类型检查主要涉及两个 API 领域:

  • 类型检索(“此节点的类型是什么?”)
  • 类型关系(“此类型是否可分配给彼类型?”)

大多数类型化 lint 规则实际上只使用了类型检索部分。仅重新实现 TypeScript 的这一部分可能会大大降低开发成本。

不幸的是,TypeScript 的类型关系部分最近被确认可供 linters 使用。typescript-eslint 将开始构建基于 checker.isTypeAssignableTo 的规则逻辑。

任何子集的 TypeScript 重新实现都需要包含现有类型检索和类型关系 API 的等价部分。

自动将 TypeScript 移植到原生速度

与其从头重新实现 TypeScript,我们能否自动将其源代码移植到更快的语言中?

TypeScript 有数万行源代码,专为单线程 JavaScript 和动态对象设计。它的架构确实不适合在另一种范式下工作。

我认为这种方法非常有希望可以得到一个始终保持最新的 TypeScript 等价物,但难度非常高。目前还没有看到有人在这个想法上取得超出概念验证的重大进展。

总结

TypeScript 本身是目前唯一稳定、可用于生产环境的 TypeScript 代码类型化 linting 工具。它垄断了可用的类型检查 API。能够使用更快的等价工具对类型化 linting 用户来说将是非常棒的,但目前还没有任何工具可以达到生产就绪的状态。

重写 TypeScript 或其等价物存在许多障碍。而且每年 TypeScript 都变得更加复杂和功能齐全。

我对最终出现一个 TypeScript 的真正竞争者充满希望,这将为生态系统带来更健康的竞争。即使没有实现这一目标,至少也可以有一个 TypeScript 子集,加速类型化 linting 等 API 消费者的速度,或者一个自动将 TypeScript 转移到原生速度的移植工具。

但要等到能够与 TypeScript 竞争的工具稳定下来,还需要一段时间。就目前而言,我们只能继续依赖 TypeScript 进行类型化 linting。