TypeScript 5.8 让我少踩了一个大坑,顺手再聊聊 --erasableSyntaxOnly

上周五下午,产品突然过来说有个接口的缓存逻辑出了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 风格的 import
  • export = 这种 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 会不会给你一些惊喜。

相关推荐
用户2930750976691 小时前
# 从零开始,打造 AI 产品的第一个关键体验——流式输出
前端·vue.js
小杍随笔1 小时前
【 .npmrc 终极配置指南:统一管理 npm/pnpm 缓存与全局目录,告别磁盘混乱】
前端·缓存·npm
默_笙1 小时前
👍 手写一个 MCP 文件读取服务:原来 AI 能"读"我的文件,全靠这个协议
前端·javascript
饮茶三千1 小时前
基于 WangEditor 的富文本编辑器定制实践
前端·vue.js·设计模式
kyle~1 小时前
前端 --- Toast 轻量级弱反馈提示组件
前端
玉宇夕落1 小时前
Vue3 + 流式输出学习
前端
努力成为AK大王1 小时前
CSS 入门完整笔记
前端·css
微笑挖矿2 小时前
大模型流式渲染:为什么"逐字显示"远比你想的复杂
前端
大流星2 小时前
LangChainJS之Chain链(四)
javascript·langchain