TypeScript 5.8 新特性详解

2025-03-25 14:14 更新

返回表达式分支的细致检查

想象一下下面的代码:

declare const untypedCache: Map<any, any>;
function getUrlObject(urlString: string): URL {
    return untypedCache.has(urlString) ?
        untypedCache.get(urlString) :
        urlString;
}

这段代码的目的是从缓存中获取 URL 对象,如果不存在就创建一个新的 URL 对象。但这里有个问题:我们忘记用输入实际构造一个新的 URL 对象了。遗憾的是,TypeScript 通常无法捕捉到这种错误。

当 TypeScript 检查像 cond ? trueBranch : falseBranch 这样的条件表达式时,它的类型会被视为两个分支类型的联合。换句话说,它会获取 trueBranchfalseBranch 的类型,然后将它们组合成一个联合类型。在这个例子中,untypedCache.get(urlString) 的类型是 any,而 urlString 的类型是 string。问题就出在这里,因为 any 类型与其他类型交互时会“感染”其他类型。联合类型 any | string 会被简化为 any,所以当 TypeScript 开始检查 return 语句中的表达式是否与预期的返回类型 URL 兼容时,类型系统已经丢失了能够捕捉到此代码中错误的信息。

在 TypeScript 5.8 中,类型系统对直接位于 return 语句内的条件表达式进行了特殊处理。条件的每个分支都会被检查是否与包含函数的声明返回类型(如果存在)兼容,因此类型系统可以捕捉到上面示例中的错误。

declare const untypedCache: Map<any, any>;
function getUrlObject(urlString: string): URL {
    return untypedCache.has(urlString) ?
        untypedCache.get(urlString) :
        urlString; // 错误!类型“string”不能分配给类型“URL”。
}

这一改动是作为更广泛的未来改进的一部分在此拉取请求中完成的。


--module nodenext 下支持对 ECMAScript 模块的 require()

多年来,Node.js 支持 ECMAScript 模块(ESM)和 CommonJS 模块共存。但两者之间的互操作性存在一些挑战:

  • ESM 文件可以 import CommonJS 文件
  • CommonJS 文件 不能 require() ESM 文件

换句话说,从 ESM 文件中使用 CommonJS 文件是可行的,但反过来则不行。这给希望提供 ESM 支持的库作者带来了许多挑战。这些库作者要么不得不与 CommonJS 用户打破兼容性,要么“双重发布”他们的库(为 ESM 和 CommonJS 提供单独的入口点),要么无限期地停留在 CommonJS 上。虽然双重发布听起来像是一个不错的折中方案,但它是一个复杂且容易出错的过程,还会使包中的代码量大致翻倍。

Node.js 22 放宽了一些限制,允许从 CommonJS 模块中 require("esm") 调用 ECMAScript 模块。Node.js 仍然不允许对包含顶级 await 的 ESM 文件进行 require(),但大多数其他 ESM 文件现在可以从 CommonJS 文件中使用。这为库作者提供了一个重大机会,使他们可以在不双重发布库的情况下提供 ESM 支持。

TypeScript 5.8 在 --module nodenext 标志下支持这种行为。当启用了 --module nodenext 时,TypeScript 将避免对这些对 ESM 文件的 require() 调用发出错误。

由于此功能可能会回退到 Node.js 的较早版本,目前没有稳定的 --module nodeXXXX 选项启用此行为;然而,我们预计 TypeScript 的未来版本可以在 node20 下稳定此功能。在此期间,我们鼓励使用 Node.js 22 及更高版本的用户使用 --module nodenext,而库作者和使用较早 Node.js 版本的用户应继续使用 --module node16(或进行小更新到 --module node18)。

有关更多信息,请参阅我们对 require("esm") 的支持文档。

--module node18

TypeScript 5.8 引入了一个稳定的 --module node18 标志。对于那些坚持使用 Node.js 18 的用户,此标志提供了一个稳定的参考点,不包含 --module nodenext 中的某些行为。具体来说:

  • node18 下禁止对 ECMAScript 模块的 require(),但在 nodenext 下允许
  • node18 下允许导入断言(已弃用,改为导入属性),但在 nodenext 下禁止

有关更多信息,请参阅 --module node18 拉取请求以及对 --module nodenext 的更改。


--erasableSyntaxOnly 选项

最近,Node.js 23.6 取消了对直接运行 TypeScript 文件的实验性支持的标记;然而,只有在该模式下支持某些构造。Node.js 取消了对 --experimental-strip-types 的标记,这要求任何 TypeScript 特定的语法不能具有运行时语义。换句话说,必须能够轻松地“擦除”或“剥离”文件中的任何 TypeScript 特定语法,留下一个有效的 JavaScript 文件。

这意味着像以下这样的构造是不被支持的:

  • enum 声明
  • 具有运行时代码的 namespacemodule
  • 类中的参数属性
  • 非 ECMAScript import =export = 赋值

这里有一些不工作的示例:

// ❌ 错误:一个 `import ... = require(...)` 别名
import foo = require("foo");

// ❌ 错误:一个具有运行时代码的命名空间
namespace container {}

// ❌ 错误:一个 `import =` 别名
import Bar = container.Bar;

class Point {
    // ❌ 错误:参数属性
    constructor(public x: number, public y: number) { }
}

// ❌ 错误:一个 `export =` 赋值
export = Point;

// ❌ 错误:一个枚举声明
enum Direction {
    Up,
    Down,
    Left,
    Right,
}

像 ts-blank-space 或 Amaro(Node.js 中类型剥离的底层库)这样的类似工具也有相同的限制。如果这些工具遇到不符合要求的代码,它们会提供有用的错误消息,但你仍然只有在实际尝试运行代码时才会发现代码不工作。

这就是为什么 TypeScript 5.8 引入了 --erasableSyntaxOnly 标志。当启用此标志时,TypeScript 会对具有运行时行为的大多数 TypeScript 特定构造发出错误。

class C {
    constructor(public x: number) { }
    //          ~~~~~~~~~~~~~~~~
    // 错误!当启用了 'erasableSyntaxOnly' 时,此语法不允许。
}

通常,你会想要将此标志与 --verbatimModuleSyntax 结合使用,这可以确保模块包含适当的导入语法,并且不会发生导入省略。

有关更多信息,请参阅此处的实现。


--libReplacement 标志

在 TypeScript 4.5 中,我们引入了用自定义文件替换默认 lib 文件的可能性。这是基于从名为 @typescript/lib-* 的包中解析库文件的可能性。例如,你可以通过以下 package.jsondom 库锁定到 @types/web 包的特定版本:

{
    "devDependencies": {
        "@typescript/lib-dom": "npm:@types/web@0.0.199"
    }
}

安装后,应该存在一个名为 @typescript/lib-dom 的包,TypeScript 在 dom 被你的设置隐含时会一直查找它。

这是一个强大的功能,但也增加了一些额外的工作。即使你不使用此功能,TypeScript 也会一直进行此查找,并且必须监视 node_modules 中的更改,以防一个 lib 替换包开始存在。

TypeScript 5.8 引入了 --libReplacement 标志,允许你禁用此行为。如果你不使用 --libReplacement,现在可以使用 --libReplacement false 禁用它。在未来,--libReplacement false 可能会成为默认值,因此如果你目前依赖此行为,应该考虑使用 --libReplacement true 明确启用它。

有关更多信息,请参阅此处的更改。


在声明文件中保留计算属性名称

为了使类中的计算属性在声明文件中具有更可预测的发出,TypeScript 5.8 将始终保留实体名称(bareVariablesdotted.names.that.look.like.this)在类中的计算属性名称中。

例如,考虑以下代码:

export let propName = "theAnswer";
export class MyClass {
    [propName] = 42;
    //  ~~~~~~~~~~
    // 错误!类属性声明中的计算属性名称必须具有简单的字面量类型或 'unique symbol' 类型。
}

TypeScript 的早期版本在为此模块生成声明文件时会发出错误,并且会生成一个尽力而为的声明文件,其中包含一个索引签名。

export declare let propName: string;
export declare class MyClass {
    [x: string]: number;
}

在 TypeScript 5.8 中,示例代码现在被允许,发出的声明文件将与你编写的代码匹配:

export declare let propName: string;
export declare class MyClass {
    [propName]: number;
}

请注意,这不会在类上创建静态命名的属性。你仍然会得到一个实际上像 [x: string]: number 这样的索引签名,因此对于这种情况,你需要使用 unique symbol 或字面量类型。

请注意,编写此代码在 --isolatedDeclarations 标志下过去和现在都是错误;但我们预计,由于此更改,计算属性名称通常将被允许在声明发出中。

请注意,使用 TypeScript 5.8 编译的文件可能会生成一个在 TypeScript 5.7 或更早版本中不向后兼容的声明文件。

有关更多信息,请参阅实现此功能的拉取请求。


在程序加载和更新上的优化

TypeScript 5.8 引入了许多优化,这些优化可以同时改进构建程序的时间,以及在 --watch 模式或编辑器场景中基于文件更改更新程序的时间。

首先,TypeScript 现在避免了在规范化路径时涉及的数组分配。通常,路径规范化会将路径的每个部分分割成一个字符串数组,根据相对段规范化路径,然后使用规范分隔符将它们重新连接。对于包含许多文件的项目,这可能是一项重要且重复的工作。TypeScript 现在避免分配数组,而是更直接地在原始路径的索引上操作。

此外,当进行不会改变项目基本结构的编辑时,TypeScript 现在避免重新验证提供给它的选项(例如 tsconfig.json 的内容)。这意味着,例如,一个简单的编辑可能不需要检查项目的输出路径是否与输入路径冲突。相反,可以使用上次检查的结果。这应该使大型项目中的编辑感觉更响应迅速。


值得注意的行为更改

这一部分突出了作为任何升级的一部分应该被承认和理解的一组值得注意的更改。有时它会突出弃用、删除和新限制。它还可以包含功能改进的错误修复,但也可能通过引入新错误来影响现有构建。

lib.d.ts

DOM 生成的类型可能会对你的代码库的类型检查产生影响。有关更多信息,请参阅与此版本的 TypeScript 相关的 DOM 和 lib.d.ts 更新的问题。


--module nodenext 下对导入断言的限制

导入断言是 ECMAScript 的提议添加,以确保导入的某些属性(例如,“此模块是 JSON,而不是可执行的 JavaScript 代码”)。它们被重新设想为一个名为导入属性的提议。作为过渡的一部分,它们从使用 assert 关键字改为使用 with 关键字。

// 一个导入断言 ❌ - 与大多数运行时不兼容。
import data from "./data.json" assert { type: "json" };

// 一个导入属性 ✅ - 导入 JSON 文件的推荐方式。
import data from "./data.json" with { type: "json" };

Node.js 22 不再接受使用 assert 语法的导入断言。因此,当在 TypeScript 5.8 中启用 --module nodenext 时,如果遇到导入断言,TypeScript 将发出错误。

import data from "./data.json" assert { type: "json" };
//                             ~~~~~~
// 错误!导入断言已被导入属性取代。请使用 'with' 而不是 'assert'。

有关更多信息,请参阅此处的更改。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号