Language:Chinese VersionEnglish Version

TypeScript 已经达到了一种令人不安的成熟阶段。那些曾经主导”我们是否应该使用它”讨论的论点——它是否会减慢迭代速度,工具链是否值得投入开销,团队能否足够快地掌握它——在很大程度上已经得到解决。取代这些论点的是一些更有趣、更坦诚的内容:TypeScript 的 TypeScript 2026 变化改进痛点不再关乎语言本身是否优秀。它们关乎围绕该语言构建的生态系统是否已经足够成熟,能够匹配语言的实际复杂性,以及工具链是否跟上了过去五年 TypeScript 开发者所设定的雄心目标。

简短的答案是:2026 年的 TypeScript 比以往任何时候都更好,但它仍然存在严重问题,社区一直在为这些问题创建变通方案而不是直接修复。这两点同时成立,理解每个问题属于哪个类别,决定了你是能够高效地使用 TypeScript,还是在周二下午调试那些本不该存在的类型错误。


TypeScript 5.x 实际带来了什么

TypeScript 5.x 的发布周期为该语言带来了从业者多年以来一直期待的功能。其中三个功能以有意义且非平凡的方式改变了实际代码库的编写方式。

装饰器:终于标准化了

TypeScript 实验性的装饰器支持以其遗留形式存在了如此之久,以至于许多开发者已经不再将其视为稳定功能。实验性标志一直是一个持续的警告,表明某些东西可能在您不知情的情况下发生变化。TypeScript 5.0 发布了与 TC39 第 3 阶段提案保持一致的装饰器,这在实践中的差异是显著的。

新的装饰器比旧版本受到更多限制,这是一件好事。旧版装饰器允许装饰器工厂对类或属性描述符执行几乎任何操作,这使得它们功能强大,但在复杂情况下使得跨装饰类的类型推断实际上变得不可能。TC39 装饰器基于定义良好、更窄的契约运行。像 NestJS 这样多年来一直依赖旧版装饰器支持框架,正处于迁移过程中,一旦完成,它们的类型安全性将显著提高。

现实的警告是,生态系统正处于过渡中期。如果您在 2026 年启动一个新项目,请使用新的装饰器。如果您正在维护一个 2022 年构建的 NestJS 应用程序,您将面临一个不太容易安排的迁移。共存期是混乱的。

常量类型参数

类型参数上的 const 修饰符 — 在 TypeScript 5.0 中引入 — 解决了一个推断问题,这个问题产生了一系列没人满意的变通方案。在这个功能之前,将字面量数组传递给泛型函数会产生推断类型 string[],而不是字面量字符串类型的元组。解决方法是在调用处添加 as const,这虽然可行,但会使调用代码充斥着类型处理逻辑,而这些本应由函数签名处理。

使用 const 类型参数,函数可以声明其泛型参数应推断为字面量类型。调用处保持简洁。这对于工具函数、配置构建器和路由库最为重要,在这些场景中保留字面量类型对下游类型推断至关重要。React Router 和 tRPC 都从中受益,最终用户可以看到自动完成和错误消息的改进。

Satisfies 运算符

satisfies 运算符出现在 TypeScript 4.9 中,它在实际代码库中的采用情况表明它很好地解决了一个真正的痛点。它解决的问题:你想要验证对象是否符合某个类型,但不希望类型注解扩大推断的类型。当你编写 const config: Config = { ... } 时,TypeScript 会将 config 的推断类型扩大为 Config,丢失了右侧的字面量类型信息。当你编写 const config = { ... } satisfies Config 时,TypeScript 会根据 Config 验证形状,但保留更窄的推断类型。

这在配置对象中特别有用,因为你既需要验证又需要完整的类型推断。该运算符迅速成为惯用用法,这表明它解决了人们实际需要解决的问题,而不是团队认为”有会很好”的功能。


tsc 的 Go 重写:基准测试数字的含义

微软宣布用 Go 重写 TypeScript 编译器是 2025 年最重要的 TypeScript 新闻。性能声明确实令人印象深刻 — 基于 Go 的编译器在大型代码库上的类型检查速度比原来快 10 到 20 倍,具体取决于项目结构。对于一个之前需要 45 秒才能完成完整类型检查的代码库,现在只需 3-4 秒。

影响范围不仅仅是构建管道优化。当类型检查速度缓慢时,开发者会调整行为以避免不必要地触发它。他们更少运行 tsc,更多地依赖编辑器反馈(编辑器运行的是具有不同约束的语言服务器),并将类型错误的发现推迟到 CI。当类型检查速度快到可以在每次保存时运行时,它就成为了编辑-运行周期的一部分,而不是周期性的批处理任务。这种行为变化对代码质量和开发者如何思考他们的类型系统产生了实际影响。

有一些重要的注意事项。Go 编译器针对常见情况与原始 tsc 保持相同的行为,但 TypeScript 类型系统中存在一些边缘情况,这些情况非常微妙,导致两种实现在某些场景下会产生不同的结果。TypeScript 团队已承诺保持兼容性,但”在实际应用中与 tsc 兼容”并不等同于”在所有边缘情况下输出完全相同的结果”。复杂的条件类型、深度嵌套的映射类型和不寻常的模板字面量构造是最可能出现差异的类别。

对于大多数项目,Go 编译器将是一个纯粹的改进,不会带来行为变化。对于核心抽象中包含复杂类型操作的项目,在切换前应进行仔细的验证。迁移路径是选择加入,而非自动进行。


模块解析:更好,而非完美

TypeScript 5.x 中的模块解析改进——特别是 bundler 解析模式和对 NodeNext 的改进——解决了多年来一直让社区感到沮丧的实际问题。bundler 模式正确地模拟了 Vite、esbuild 和类似工具如何处理导入,这之前要么需要使用不正确的解析模式,要么忍受那些不反映实际运行时行为的虚假类型错误。

NodeNext 解析模式强制在相对导入上使用显式文件扩展名以匹配 Node 的原生 ESM 行为,这在技术上是正确的,但在实践中令人烦恼。在包含 utils.ts 的 TypeScript 文件中编写 import { foo } from './utils.js' 是一种前向引用,在你理解其存在原因之前感觉是错误的,即使理解之后仍然感觉有点不对。这个模式是正确的,但开发者体验仍然尴尬。

路径别名 — @/components~/utils — 在 TypeScript、bundler 和有时测试运行器中仍需要单独配置。这是三个必须相互一致的独立配置文件,而 TypeScript 的配置不会自动传播到其他工具。这是一个协调问题,而不是 TypeScript 特有的缺陷,但 TypeScript 是开发者首先接触且最容易归咎的工具。


仍然存在的问题

tsconfig.json 的混乱

2026 年的一个新 TypeScript 项目,如果打算同时支持 Web 前端和 Node.js 后端,使用路径别名,运行 Vitest 进行测试,并输出正确的 ESM,则至少需要三个 tsconfig 文件:一个基础配置,一个扩展基础配置用于浏览器构建,另一个扩展基础配置用于 Node。Vestest 需要自己的配置文件,因为它需要不同的设置来发现测试。你选择的框架的单体仓库设置指南又增加了一层扩展链。很快你就会拥有一个包含五个 tsconfig 文件的层次结构,以及一个需要理解哪个文件控制哪些代码的非平凡心智模型。

这并不是说 TypeScript 的配置系统设计得很糟糕。大多数选项的存在是因为它们所代表的底层差异是真实的。moduleResolutionmoduletargetlib 是四个真正相互独立的不同关注点。问题在于 tsconfig 的表面积已经增长到典型项目没有明显正确答案的程度,错误的组合会产生被诊断为代码问题但实际上是配置问题的错误。

tsconfig/bases 社区包为常见环境提供了有主见的起始配置,是目前务实的解决方案。它不是一个解决方案——它是一种约定,将配置问题简化为”扩展正确的基配置并仅覆盖不同的部分”。这种方法在失效前一直有效,而失效的情况就是当你发现你的用例与基配置的差异足够大,以至于需要理解所有选项的实际作用。

ESM/CJS 互操作性仍然一团糟

ESM/CJS 互操作性问题并非 TypeScript 独有——它是整个 JavaScript 生态系统的问题——但 TypeScript 在底层的运行时混乱之上增加了自己的一层复杂性。对于通过 package.json exports 字段提供 CJS 和 ESM 入口点的包,其类型声明需要特定的 tsconfig 设置才能正确解析。没有为 ESM 入口点发布适当类型声明的包会导致看起来像是缺少类型但实际上是模块解析失败的错误。

“双CJS/ESM包”模式曾是连接旧世界和新世界的一种合理桥梁,但它产生了一类仅在运行时才能发现的微妙bug。TypeScript可以告诉你某个类型是否可用,但它无法可靠地告诉你打包器在运行时实际加载的是包的CJS版本还是ESM版本,以及这种区别对你的特定用例是否重要。

在这方面,Bun值得称赞。Bun的TypeScript支持不需要单独的编译步骤,透明地处理CJS和ESM导入,并且直接运行TypeScript,无需Node.js强加的tsconfig-to-runtime翻译层。对于Bun兼容性保证足够的应用程序,ESM/CJS问题基本消失。但对于需要在Node.js上运行的特定库,或者与Bun不兼容的项目依赖,互操作问题仍未解决。

Monorepo设置痛点

在monorepo中设置TypeScript——其中仓库中的包需要相互导入并保持适当的类型检查——需要使用项目引用。项目引用是正确的方法,它们确实有效,但配置过程相当繁琐。每个包需要一个tsconfig来声明其对其他包的引用。根tsconfig需要声明所有包。构建工具需要理解引用图。当你添加新包时,需要在多个地方更新多个文件。

Turborepo和Nx都提供了减少这种配置开销的抽象,但它们通过添加自己的配置界面和学习曲线来实现这一点。根本问题——TypeScript的多项目理解需要依赖图的显式声明——不会消失,因为它对增量编译性能至关重要。成本是真实的,收益也是真实的,两者都不会消失。

类型体操和认知开销

TypeScript的类型系统异常强大。条件类型、映射类型、模板字面量类型、条件类型中的infer、递归类型别名——这些特性共同使你能够表达在其他大多数静态类型语言中不可能实现的类型关系。它们也让你能够编写需要45分钟才能理解的类型,并产生400行长度的错误信息。

TypeScript 代码库中的激励机制往往推动复杂化的发展。当一个类型不太合适时,最容易的解决方案通常是添加另一层条件逻辑,而不是重新考虑抽象。库作者尤其面临为每种使用模式生成完美类型推断的压力,这产生了比其描述的运行时代码更难维护的类型级别代码。一些实用类型库包含排序算法和状态机的类型级别实现——这并非因为有用,而是因为类型系统在技术上使其成为可能。

TypeScript 代码库在这方面走得太远的信号通常是关键位置出现 any 逃生舱口。当一个类型变得足够复杂,正确维护它的成本超过了它提供的类型安全时,开发者就会求助于 any——而那个 any 会传播开来。核心实用类型中的单个 any 可能会在整个代码库中产生不可见的类型漏洞。unknown 是一种有原则的替代方案,它强制显式缩小范围,但它需要更多代码,并不能解决过度复杂类型的根本问题。


运行时验证:Zod、Valibot 以及为什么你需要两者

TypeScript 最根本的限制之一是类型仅在编译时存在。一个从外部 API 接收 JSON 的 TypeScript 应用程序,在开发者编写类型注断并断言它之前,对该数据没有任何类型信息。如果运行时结构不匹配编译时注断,TypeScript 无法检测到这一点——断言只是一种声明,而非保证。

Zod 和 Valibot 通过提供模式定义解决了这个问题,这些定义从单一源生成 TypeScript 类型和运行时验证器。用户对象的 Zod 模式产生推断的 TypeScript 类型和运行时解析函数,该函数验证形状并在失败时抛出异常。这是正确的模型:类型安全要求编译时和运行时表示保持同步,而分别维护它们是导致分歧的根源。

Zod 的采用程度已经相当高,以至于它实际上已成为 API 边界输入验证的标准做法。tRPC 使用 Zod 进行过程输入模式定义。Next.js Server Actions 文档推荐它用于表单验证。主要的批评——即 Zod 的包大小大于替代品——催生了 Valibot,它通过可摇树优化的模块和更小的占用空间实现了相同的概念目标。对于包大小重要的边缘运行时和无服务器函数,Valibot 是一个合理的选择。对于其他所有情况,Zod 更大的生态系统和更完整的文档使其具有优势。


Effect-TS 和函数式 TypeScript 运动

Effect-TS 在 TypeScript 社区中处于一个两极分化的位置。它是一个全面的函数式编程框架,将效果(异步操作、错误、依赖)建模为可以组合和推理的值。它提供的类型安全性确实令人印象深刻:错误类型在每项操作的类型签名中被跟踪,依赖注入经过类型检查,并发原语以一种能让编译器发现数据竞争的方式表达。

代价是需要投入大量学习成本,并且编程模型与惯用的 TypeScript 有显著差异。用 Effect 编写的代码看起来像是将 Haskell 的 IO monad 移植到了 TypeScript 中。对于有函数式编程背景的开发者来说,这是一个特点。而对于主要从 React 或 Node.js 环境学习 TypeScript 的团队来说,学习曲线陡峭到足以产生持续的抵触情绪。

实际问题是 Effect 的保证是否值得团队付出的成本。对于那些错误处理确实复杂的系统——当一个服务需要进行多个可能以不同方式失败的外部调用,当重试逻辑和断路器需要可预测地组合时——Effect 的模型提供了 try/catch 方法无法实现的真正价值。对于具有简单错误处理的 CRUD API,Effect 带来了不成比例的复杂性而没有相应的收益。社区向 Effect 发展的趋势是真实的,但不应该被误解为普遍推荐。


Bun vs Node.js 在 TypeScript 开发中的对比

在 2026 年,对于 TypeScript 项目,Bun 和 Node.js 之间的实际开发体验差异是显著的。Bun 直接运行 TypeScript,无需单独的编译步骤,这消除了等待 ts-nodetsx 在执行前进行转译的反馈循环摩擦。Bun 内置的测试运行器、打包器和包管理器默认都支持 TypeScript。对于一个全新的 TypeScript 优先项目,Bun 的集成工具产生的配置比 Node.js 等效方案简单得多。

Node.js 22 和 23 添加了实验性的原生 TypeScript 剥离功能,它通过移除类型注解来运行 TypeScript 文件,而不运行 TypeScript 编译器。这很快——比 ts-node 更快——但它不进行类型检查。它只是对 TypeScript 表面语法的语法支持,没有语义保证。那些切换到原生 Node.js TypeScript 支持并禁用单独类型检查运行的开发者实际上不是在运行 TypeScript;他们只是在运行带有语法高亮的 JavaScript。

Bun 的真正限制在于兼容性覆盖范围。大多数流行的 Node.js 包在 Bun 中都能正常工作,兼容性覆盖范围已经显著扩大。但”大多数”不等于”全部”,最有可能出现兼容性问题的往往是原生插件、具有特殊模块加载模式的遗留 CJS 包,以及依赖 Node.js 特定内部模块的包。在 Bun 上进行生产部署前,应该针对特定的依赖列表进行兼容性审计,而不是仅仅假设它会正常工作。


2026年你该选择 TypeScript 吗?

对于有多名贡献者的项目、预期维护时间超过六个月的项目、领域逻辑复杂到能通过类型获得文档价值的项目,以及团队规模会增长或变动的项目:应该选择。在这些情况下选择 TypeScript 的论点并非出于意识形态。而是因为 TypeScript 能让某些类型的错误变得不可能发生,使重构更安全,并让开发者阅读不熟悉的代码时能更快上手。这些好处会随时间累积,在项目开始时难以察觉,但一年后就变得显而易见。

需要注意的是,只有在使用类型系统时深思熟虑,TypeScript 的好处才能实现。一个到处使用 any、类型技术上正确但过于狭窄而无法发挥作用,或者包含团队中无人完全理解的复杂类型操作的代码库,并不比编写良好的 JavaScript 更安全——反而更不安全,因为编译时检查带来的虚假信心与实际运行时行为(而类型并未真正描述这种行为)并存。

何时普通 JavaScript 实际上就足够了

确实存在 TypeScript 带来开销但没有相应回报的情况。生命周期短的脚本——自动化脚本、一次性数据迁移、简单的 CLI 工具——通常属于这一类。类型永远不会捕获真正的错误,因为脚本只运行一次就会被丢弃。编写脚本的开发者知道数据的结构,因为他们五分钟前刚创建它。添加 TypeScript 会增加编译步骤、一个 tsconfig 文件,以及偶尔出现的库导入类型错误,脚本作者必须在脚本运行前解决这些错误。

原型开发也符合这种情况。当主要目标是快速探索问题空间时,TypeScript 的类型系统可能成为阻碍探索的摩擦力。许多有经验的 TypeScript 开发者会用 JavaScript 编写探索性代码,当探索出值得保留的内容时再添加 TypeScript。这不是一种失败模式——这是对 TypeScript 可选性质的合理利用。

对于真正拥有简单领域模型的个人项目——个人网站、只有三个端点的小型 API、每晚运行的脚本——如果开发者足够熟练,能够保持对传递数据结构的清晰理解,那么使用 JavaScript 是合理的。诚实的说法是:TypeScript 的益处与团队规模、代码库规模和复杂程度成正比。对于一个完全可以在脑海中掌握的代码库,TypeScript 的益处较小,而开销则相对较大。


语言的真实状态

2026 年的 TypeScript 是一个成熟的工具,拥有一套成熟的权衡方案。Go 重写将显著改善编译时体验,而不仅仅是增量式加速。装饰器、常量类型参数和 satisfies 操作符的稳定化解决了真实的表达能力缺口。运行时验证生态系统——Zod、Valibot 以及它们代表的模式优先方法——为 TypeScript 提供了一个对编译时限制的有力回应,而这个限制一直是一个合理的批评。

没有改变的是,TypeScript 的配置复杂性确实很高,ESM/CJS 转换在生态系统层面仍然是一个未解决的问题,monorepo 设置需要的仪式感超过了应有的程度,而类型系统的强大创造了一种过度复杂化的激励,团队必须积极抵制。这些不是正在修复过程中的缺陷。它们是生态系统的结构性特征,实践者需要管理这些特征,而不是等待其他人解决。

对于大多数生产代码库来说,是否选择 TypeScript 已经不再是问题。问题是如何使用它——具体来说,是有意识地、足够务实地去使用,避免类型系统复杂性变成无人维护的第二个代码库。2026 年最能充分利用 TypeScript 的团队,是将类型系统视为沟通工具,而不是类型级编程复杂度证明的团队。


关键要点

  • TypeScript 5.x 稳定了装饰器(与 TC39 保持一致),添加了 const 类型参数以实现更好的字面量推断,并引入了 satisfies 用于在不扩展类型的情况下进行验证。
  • tsc 基于 Go 的重写为大型代码库提供了 10-20 倍更快的类型检查速度,改变了开发者将类型检查集成到工作流中的方式,而不仅仅是加速 CI。
  • tsconfig 复杂性、ESM/CJS 互操作性和 monorepo 设置仍然是 TypeScript 项目中最常见的三个摩擦源,这些问题都远未得到完全解决。
  • any 逃生舱是一种症状而非解决方案——当类型变得过于复杂难以维护时,使用显式缩小的 unknown 是有原则的替代方案。
  • Zod 和 Valibot 提供了 TypeScript 编译时类型无法提供的运行时验证层,在任何数据形状无法保证的 API 边界上都应成为标准。
  • Bun 的原生 TypeScript 支持显著降低了新项目的工具复杂性;Node.js 的实验性 TypeScript 剥离只是语法支持,而非类型检查。
  • TypeScript 仍然是大多数团队和大多数项目的正确默认选择;对于脚本、原型和简单的单人项目,纯 JavaScript 确实是合适的,因为其开销超过了收益。

Michael Sun 是 NovVista 的开发者兼作者,关注开发者工具、基础设施以及随时间累积的工程决策。他维护过从单人项目到多团队 monorepo 的各种 TypeScript 代码库,并且有 tsconfig 的”伤疤”可以证明。

By Michael Sun

Founder and Editor-in-Chief of NovVista. Software engineer with hands-on experience in cloud infrastructure, full-stack development, and DevOps. Writes about AI tools, developer workflows, server architecture, and the practical side of technology. Based in China.

Leave a Reply

Your email address will not be published. Required fields are marked *

You missed