上周五下午,产品突然过来说有个接口的缓存逻辑出了bug,用户看到的链接和实际跳转的不一样。我排查了半天,最后发现是一个三元表达式埋的坑------一边返回了 string,一边返回的其实是 any,结果 any 把类型检查全吞了。正当我准备改代码的时候,同事提醒我:"升一下 TypeScript 到 5.8 试试,这个版本专门修了这种问题。"我半信半疑地升级,然后运行 tsc ------报错信息直接指出了三元表达式里 string 不能赋值给 URL。说实话,这个体验让我有点意外。TypeScript 这波操作让我觉得它终于像个"真正的类型检查器"了,而不是一个会漏报错误的橡皮图章。这篇文章就聊聊 TypeScript 5.8 的几个让我觉得"这版本没白升"的特性。
那个让我少写一堆类型守卫的 conditional return-type narrowing
先说最让我有感的这个改进。 之前写代码的时候,三元表达式里的类型检查一直是让我头疼的事情。看这个例子:
typescript
declare const cache: Map<string, any>;
function getUrl(url: string): URL {
return cache.has(url) ? cache.get(url) : url;
}
我本意是从缓存里拿 URL 对象,如果缓存没有就拿字符串自己造一个。但 cache.get(url) 返回的是 any,而 any | string 会变成 any,所以 TypeScript 之前根本不会报错。这个 bug 就这么埋进去了,在线上跑了三个月才被产品发现。 TypeScript 5.8 之前,这段代码是"合法"的,tsc 不会报任何错误。 TypeScript 5.8 之后,tsc 直接报错:
python
error TS2322: Type 'string' is not assignable to type 'URL'.
因为 5.8 会单独检查三元表达式每个分支的返回类型,而不是把它们合成一个 union 之后再来检查。这意味着你写的 cond ? A : B,TypeScript 现在会分别检查 A 能不能赋值给返回值类型、B 能不能赋值给返回值类型。 这个改进在日常写代码的时候特别有用。举个例子,我之前为了避免这种问题,经常会写类型守卫:
javascript
function getUrl(url: string): URL {
const cached = cache.get(url);
if (cached) {
if (cached instanceof URL) {
return cached;
}
throw new Error('Invalid cache value');
}
return new URL(url);
}
现在有了 conditional return-type narrowing,上面这种防御性代码可以省掉很多。三元表达式直接写,TypeScript 帮你兜底:
php
function getUrl(url: string): URL {
const cached = cache.get(url);
return cached instanceof URL ? cached : new URL(url);
}
干净多了,而且不会漏掉类型检查。
--erasableSyntaxOnly:给 Node.js 直接跑 TS 铺路
如果说 conditional return-type narrowing 是"查漏补缺",那 --erasableSyntaxOnly 就是 TypeScript 5.8 的"战略级"特性。 Node.js 23.6 开始支持直接运行 TypeScript 文件了,用的是 --experimental-strip-types 参数。但这里有个限制:TS 文件里不能有"有运行时行为的 TS 特有语法"。 什么是有运行时行为的 TS 特有语法?比如:
enum声明namespace(带运行时代码的)- 类的参数属性(
constructor(public x: number)) import = require()这种 CommonJS 风格的 importexport =这种 CommonJS 风格的 export
如果你在项目里用了这些语法,Node.js 的 --experimental-strip-types 直接报错,不管你是想直接跑还是先 strip 再跑。 TypeScript 5.8 新增了 --erasableSyntaxOnly 标志,就是让你在编译阶段就能发现"这段代码将来没法被 Node.js 直接运行"的问题。
typescript
class Point {
constructor(public x: number, public y: number) { }
}
开启 --erasableSyntaxOnly 之后,tsc 直接报错:
csharp
error: This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
我的体感是,这个特性对两种人特别有用:
第一种是想迁移到"Node.js 原生支持 TS"的团队。 如果你正在规划从 ts-node 迁移到 Node.js 内置的 TS 支持,这个 flag 可以帮你提前扫掉那些会在未来报错的代码,不用等到 Node 真的跑不起来再回来改。
第二种是想保持代码"现代化"的开发者。 --erasableSyntaxOnly 本质上是在推动大家用标准 ECMAScript 语法,比如用 const enum 或者对象替代 enum、用 import type 替代 import assertion。这些改动长期来看对代码健康是有好处的。
官方文档建议把 --erasableSyntaxOnly 和 --verbatimModuleSyntax 搭配使用。--verbatimModuleSyntax 会确保 import 语句的格式符合预期,import elision 也不会乱来。这样一套组合拳打下来,你的代码基本上就是"strip-types safe"的了。
--module node18 终于稳定了
之前如果你用的是 Node.js 18,配置 module 选项的时候其实挺尴尬的。 node16 太老,不支持新特性。nodenext 虽然最激进,但行为可能会随着 Node.js 版本变化,不够稳定。你想用 18 的特性但又不想被"未来的 nodenext 行为变化"影响?之前没有好的选择。 TypeScript 5.8 推出了稳定的 --module node18 选项。现在你可以在 tsconfig.json 里直接写:
json
{
"compilerOptions": {
"module": "node18"
}
}
这个选项有几个明确的边界:
- 不支持从 CJS
require()ESM 模块(这个只在 nodenext 里支持) - 允许 import assertions(但 nodenext 已经废弃了,改用 import attributes)
简单说,--module node18 就是给"不想折腾,想稳定用 Node 18"的人准备的。如果你在用 Node.js 18,这应该是目前最稳妥的 module 配置。
--libReplacement false:小改动,但省心
这个改动比较低调,但我挺喜欢的。 TypeScript 4.5 引入了 lib 文件替换功能,可以把默认的 lib.d.ts 换成自定义版本。比如你可以通过 @typescript/lib-dom 来指定使用某个版本的 DOM 类型定义。 但问题是,即使你根本没用这个功能,TypeScript 也会每次编译的时候去检查 node_modules 里有没有这个包存在。 TypeScript 5.8 引入了 --libReplacement 选项。如果你没在用 lib 替换功能,可以显式关闭它:
json
{
"compilerOptions": {
"libReplacement": false
}
}
这样 TypeScript 就不会每次都去做那个没意义的检查了。官方文档说,未来 --libReplacement false 可能会成为默认行为,所以如果你现在依赖这个行为,记得显式开启。
写在最后
说实话,之前每次 TypeScript 发布新版本,我都是先看 changelog 有没有 breaking changes,遇到新特性都是"观望观望"。TypeScript 5.8 算是让我稍微转变了一下态度。 conditional return-type narrowing 这个特性,解决了三元表达式类型检查这个"看起来是小问题、其实容易埋大坑"的场景。TypeScript 这次是真正在补齐类型系统的短板。 --erasableSyntaxOnly 则是一个面向未来的特性。如果你和我一样正在关注"Node.js 原生跑 TS"这个方向,这个 flag 可以让你提前做好准备,不用等到 Node 真的推了再手忙脚乱地改代码。
升级成本不高,收益是实打实的。如果你还没升级,建议找个低峰期跑一下 npm install typescript@5.8,看看 tsc 会不会给你一些惊喜。