TypeScript 5.8

返回表达式中分支的细粒度检查

¥Granular Checks for Branches in Return Expressions

考虑如下代码:

¥Consider some code like the following:

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

此代码的目的是从缓存中检索 URL 对象(如果存在),或者如果不存在,则创建一个新的 URL 对象。但是,有一个 bug:我们忘记用输入实际构造一个新的 URL 对象了。遗憾的是,TypeScript 通常无法捕获此类错误。

¥The intent of this code is to retrieve a URL object from a cache if it exists, or to create a new URL object if it doesn’t. However, there’s a bug: we forgot to actually construct a new URL object with the input. Unfortunately, TypeScript generally didn’t catch this sort of bug.

当 TypeScript 检查像 cond ? trueBranch : falseBranch 这样的条件表达式时,它的类型被视为两个分支类型的联合。换句话说,它会获取 trueBranchfalseBranch 的类型,并将它们组合成一个联合类型。在这种情况下,untypedCache.get(urlString) 的类型是 anyurlString 的类型是 string。问题就出在这里,因为 any 在与其他类型的交互上具有极强的传染性。联合 any | string 被简化为 any,因此当 TypeScript 开始检查 return 语句中的表达式是否与预期的返回类型 URL 兼容时,类型系统已经丢失了所有可以捕获此代码中错误的信息。

¥When TypeScript checks conditional expressions like cond ? trueBranch : falseBranch, its type is treated as a union of the types of the two branches. In other words, it gets the type of trueBranch and falseBranch, and combines them into a union type. In this case, the type of untypedCache.get(urlString) is any, and the type of urlString is string. This is where things go wrong because any is so infectious in how it interacts with other types. The union any | string is simplified to any, so by the time TypeScript starts checking whether the expression in our return statement is compatible with the expected return type of URL, the type system has lost any information that would have caught the bug in this code.

在 TypeScript 5.8 中,类型系统会直接在 return 语句中对条件表达式进行特殊处理。条件的每个分支都会根据包含函数的声明返回类型(如果存在)进行检查,因此类型系统可以捕获上述示例中的错误。

¥In TypeScript 5.8, the type system special-cases conditional expressions directly inside return statements. Each branch of the conditional is checked against the declared return type of the containing functions (if one exists), so the type system can catch the bug in the example above.

ts
declare const untypedCache: Map<any, any>;
function getUrlObject(urlString: string): URL {
return untypedCache.has(urlString) ?
untypedCache.get(urlString) :
urlString;
// ~~~~~~~~~
// error! Type 'string' is not assignable to type 'URL'.
}

此更改已作为 在此拉取请求中 进行,是 TypeScript 未来一系列改进的一部分。

¥This change was made within this pull request, as part of a broader set of future improvements for TypeScript.

支持在 --module nodenext 中使用 ECMAScript 模块的 require()

¥Support for require() of ECMAScript Modules in --module nodenext

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

¥For years, Node.js supported ECMAScript modules (ESM) alongside CommonJS modules. Unfortunately, the interoperability between the two had some challenges.

  • ESM 文件可以 import CommonJS 文件

    ¥ESM files could import CommonJS files

  • CommonJS 文件无法导入 require() ESM 文件

    ¥CommonJS files could not require() ESM files

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

¥In other words, consuming CommonJS files from ESM files was possible, but not the other way around. This introduced many challenges for library authors who wanted to provide ESM support. These library authors would either have to break compatibility with CommonJS users, “dual-publish” their libraries (providing separate entry-points for ESM and CommonJS), or just stay on CommonJS indefinitely. While dual-publishing might sound like a good middle-ground, it is a complex and error-prone process that also roughly doubles the amount of code within a package.

Node.js 22 放宽了部分限制,允许从 CommonJS 模块到 ECMAScript 模块的 require("esm") 调用。Node.js 仍然不允许在包含顶层 await 的 ESM 文件上使用 require(),但现在大多数其他 ESM 文件都可以从 CommonJS 文件中提取。这为库作者提供了一个重要的机会,让他们无需双重发布库即可提供 ESM 支持。

¥Node.js 22 relaxes some of these restrictions and permits require("esm") calls from CommonJS modules to ECMAScript modules. Node.js still does not permit require() on ESM files that contain a top-level await, but most other ESM files are now consumable from CommonJS files. This presents a major opportunity for library authors to provide ESM support without having to dual-publish their libraries.

TypeScript 5.8 在 --module nodenext 标志下支持此行为。启用 --module nodenext 后,TypeScript 将避免在这些 require() 调用 ESM 文件时触发错误。

¥TypeScript 5.8 supports this behavior under the --module nodenext flag. When --module nodenext is enabled, TypeScript will avoid issuing errors on these require() calls to ESM files.

由于此功能可能会被移植到旧版本的 Node.js,因此目前没有稳定的 --module nodeXXXX 选项可以启用此行为;但是,我们预测 TypeScript 的未来版本可能会在 node20 下稳定该功能。与此同时,我们鼓励 Node.js 22 及更新版本的用户使用 --module nodenext,而库作者和旧 Node.js 版本的用户则应继续使用 --module node16(或进行小更新至 --module node18)。

¥Because this feature may be back-ported to older versions of Node.js, there is currently no stable --module nodeXXXX option that enables this behavior; however, we predict future versions of TypeScript may be able to stabilize the feature under node20. In the meantime, we encourage users of Node.js 22 and newer to use --module nodenext, while library authors and users of older Node.js versions should remain on --module node16 (or make the minor update to --module node18).

更多信息请见 在此处查看我们对 require(“esm”) 的支持

¥For more information, see our support for require(“esm”) here.

--module node18

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

¥TypeScript 5.8 introduces a stable --module node18 flag. For users who are fixed on using Node.js 18, this flag provides a stable point of reference that does not incorporate certain behaviors that are in --module nodenext. Specifically:

  • ECMAScript 模块的 require()node18 下是不允许的,但在 nodenext 下是允许的。

    ¥require() of ECMAScript modules is disallowed under node18, but allowed under nodenext

  • 导入断言(已弃用,建议使用导入属性)在 node18 下可用,但在 nodenext 下不可用。

    ¥import assertions (deprecated in favor of import attributes) are allowed under node18, but are disallowed under nodenext

查看 --module node18 拉取请求对以下更改进行了更改 --module nodenext 的更多信息。

¥See more at both the --module node18 pull request and changes made to --module nodenext.

--erasableSyntaxOnly 选项

¥The --erasableSyntaxOnly Option

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

¥Recently, Node.js 23.6 unflagged experimental support for running TypeScript files directly; however, only certain constructs are supported under this mode. Node.js has unflagged a mode called --experimental-strip-types which requires that any TypeScript-specific syntax cannot have runtime semantics. Phrased differently, it must be possible to easily erase or “strip out” any TypeScript-specific syntax from a file, leaving behind a valid JavaScript file.

这意味着不支持如下构造:

¥That means constructs like the following are not supported:

  • enum 声明

    ¥enum declarations

  • 带有运行时代码的 namespacemodule

    ¥namespaces and modules with runtime code

  • 类中的参数属性

    ¥parameter properties in classes

  • 非 ECMAScript import =export = 分配

    ¥Non-ECMAScript import = and export = assignments

以下是一些无效示例:

¥Here are some examples of what does not work:

ts
// ❌ error: An `import ... = require(...)` alias
import foo = require("foo");
// ❌ error: A namespace with runtime code.
namespace container {
}
// ❌ error: An `import =` alias
import Bar = container.Bar;
class Point {
// ❌ error: Parameter properties
constructor(public x: number, public y: number) { }
}
// ❌ error: An `export =` assignment.
export = Point;
// ❌ error: An enum declaration.
enum Direction {
Up,
Down,
Left,
Right,
}

类似工具如 ts-blank-spaceAmaro(Node.js 中用于类型剥离的底层库)具有相同的限制。如果遇到不符合这些要求的代码,这些工具将提供有用的错误消息,但你仍然不会发现你的代码不起作用,直到你实际尝试运行它。

¥Similar tools like ts-blank-space or Amaro (the underlying library for type-stripping in Node.js) have the same limitations. These tools will provide helpful error messages if they encounter code that doesn’t meet these requirements, but you still won’t find out your code doesn’t work until you actually try to run it.

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

¥That’s why TypeScript 5.8 introduces the --erasableSyntaxOnly flag. When this flag is enabled, TypeScript will error on most TypeScript-specific constructs that have runtime behavior.

ts
class C {
constructor(public x: number) { }
// ~~~~~~~~~~~~~~~~
// error! This syntax is not allowed when 'erasableSyntaxOnly' is enabled.
}
}

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

¥Typically, you will want to combine this flag with the --verbatimModuleSyntax, which ensures that a module contains the appropriate import syntax, and that import elision does not take place.

更多信息请见 查看此处的实现

¥For more information, see the implementation here.

--libReplacement 标志

¥The --libReplacement Flag

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

¥In TypeScript 4.5, we introduced the possibility of substituting the default lib files with custom ones. This was based on the possibility of resolving a library file from packages named @typescript/lib-*. For example, you could lock your dom libraries onto a specific version of the @types/web package with the following package.json:

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

安装后,应该存在一个名为 @typescript/lib-dom 的包,并且当你的设置暗示使用 dom 时,TypeScript 目前会始终查找该包。

¥When installed, a package called @typescript/lib-dom should exist, and TypeScript will currently always look it up when dom is implied by your settings.

这是一个强大的功能,但也带来了一些额外的工作。即使你不使用此功能,TypeScript 也会始终执行此查找,并且必须监视 node_modules 中的变化,以防出现 lib 的替代包。

¥This is a powerful feature, but it also incurs a bit of extra work. Even if you’re not using this feature, TypeScript always performs this lookup, and has to watch for changes in node_modules in case a lib-replacement package begins to exist.

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

¥TypeScript 5.8 introduces the --libReplacement flag, which allows you to disable this behavior. If you’re not using --libReplacement, you can now disable it with --libReplacement false. In the future --libReplacement false may become the default, so if you currently rely on the behavior you should consider explicitly enabling it with --libReplacement true.

更多信息请见 查看此处的变更

¥For more information, see the change here.

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

¥Preserved Computed Property Names in Declaration Files

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

¥In an effort to make computed properties have more predictable emit in declaration files, TypeScript 5.8 will consistently preserve entity names (bareVariables and dotted.names.that.look.like.this) in computed property names in classes.

例如,考虑以下代码:

¥For example, consider the following code:

ts
export let propName = "theAnswer";
export class MyClass {
[propName] = 42;
// ~~~~~~~~~~
// error!
// A computed property name in a class property declaration must have a simple literal type or a 'unique symbol' type.
}

以前版本的 TypeScript 在为该模块生成声明文件时会报错,而尽力而为的声明文件会生成索引签名。

¥Previous versions of TypeScript would issue an error when generating a declaration file for this module, and a best-effort declaration file would generate an index signature.

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

在 TypeScript 5.8 中,现在允许使用示例代码,并且生成的声明文件将与你编写的内容匹配:

¥In TypeScript 5.8, the example code is now allowed, and the emitted declaration file will match what you wrote:

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

请注意,这不会在类上创建静态命名的属性。你最终仍然会得到类似于 [x: string]: number 的有效索引签名,因此对于该用例,你需要使用 unique symbol 或字面量类型。

¥Note that this does not create statically-named properties on the class. You’ll still end up with what is effectively an index signature like [x: string]: number, so for that use case, you’d need to use unique symbols or literal types.

请注意,在 --isolatedDeclarations 标志下,编写此代码过去和现在都是错误的;但我们预计,由于这一变化,计算属性名称通常可以在声明中被允许使用。

¥Note that writing this code was and currently is an error under the --isolatedDeclarations flag; but we expect that thanks to this change, computed property names will generally be permitted in declaration emit.

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

¥Note that it’s possible (though unlikely) that a file compiled in TypeScript 5.8 may generate a declaration file that is not backward compatible in TypeScript 5.7 or earlier.

更多信息请见 查看实现 PR

¥For more information, see the implementing PR.

程序加载和更新的优化

¥Optimizations on Program Loads and Updates

TypeScript 5.8 引入了一系列优化,这些优化既可以缩短构建程序的时间,也可以在 --watch 模式或编辑器场景下基于文件更改更新程序。

¥TypeScript 5.8 introduces a number of optimizations that can both improve the time to build up a program, and also to update a program based on a file change in either --watch mode or editor scenarios.

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

¥First, TypeScript now avoids array allocations that would be involved while normalizing paths. Typically, path normalization would involve segmenting each portion of a path into an array of strings, normalizing the resulting path based on relative segments, and then joining them back together using a canonical separator. For projects with many files, this can be a significant and repetitive amount of work. TypeScript now avoids allocating an array, and operates more directly on indexes of the original path.

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

¥Additionally, when edits are made that don’t change the fundamental structure of a project, TypeScript now avoids re-validating the options provided to it (e.g. the contents of a tsconfig.json). This means, for example, that a simple edit might not require checking that the output paths of a project don’t conflict with the input paths. Instead, the results of the last check can be used. This should make edits in large projects feel more responsive.

值得注意的行为变更

¥Notable Behavioral Changes

本节重点介绍了一系列值得注意的变更,在任何升级过程中都应确认并理解这些变更。有时它会高亮弃用、移除和新的限制。它还可以包含功能改进的错误修复,但这也可能通过引入新错误来影响现有构建。

¥This section highlights a set of noteworthy changes that should be acknowledged and understood as part of any upgrade. Sometimes it will highlight deprecations, removals, and new restrictions. It can also contain bug fixes that are functionally improvements, but which can also affect an existing build by introducing new errors.

lib.d.ts

为 DOM 生成的类型可能会对代码库的类型检查产生影响。更多信息请见 查看此版本 TypeScript 的 DOM 和 lib.d.ts 更新相关链接问题

¥Types generated for the DOM may have an impact on type-checking your codebase. For more information, see linked issues related to DOM and lib.d.ts updates for this version of TypeScript.

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

¥Restrictions on Import Assertions Under --module nodenext

导入断言是 ECMAScript 中提出的一项新增功能,用于确保导入(例如 “此模块为 JSON 格式,并非旨在作为可执行的 JavaScript 代码。“)的某些属性。它们被重新设计为名为 导入属性 的提案。作为转换的一部分,它们从使用 assert 关键字转换为使用 with 关键字。

¥Import assertions were a proposed addition to ECMAScript to ensure certain properties of an import (e.g. “this module is JSON, and is not intended to be executable JavaScript code”). They were reinvented as a proposal called import attributes. As part of the transition, they swapped from using the assert keyword to using the with keyword.

ts
// An import assertion ❌ - not future-compatible with most runtimes.
import data from "./data.json" assert { type: "json" };
// An import attribute ✅ - the preferred way to import a JSON file.
import data from "./data.json" with { type: "json" };

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

¥Node.js 22 no longer accepts import assertions using the assert syntax. In turn when --module nodenext is enabled in TypeScript 5.8, TypeScript will issue an error if it encounters an import assertion.

ts
import data from "./data.json" assert { type: "json" };
// ~~~~~~
// error! Import assertions have been replaced by import attributes. Use 'with' instead of 'assert'

详情请见 查看此处的变更

¥For more information, see the change here