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 通常不会捕捉这种类型的 bug。

🌐 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 在支持 CommonJS 模块的同时,也支持 ECMAScript 模块(ESM)。不幸的是,这两者之间的互操作性存在一些挑战。

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

  • ESM 文件可以 import CommonJS 文件
  • CommonJS 文件 无法 require() ESM 文件

换句话说,从 ESM 文件中使用 CommonJS 文件是可能的,但反过来则不行。这给希望提供 ESM 支持的库作者带来了许多挑战。这些库作者要么不得不破坏与 CommonJS 用户的兼容性,要么“二重发布”他们的库(为 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 将避免对这些对 ESM 文件的 require() 调用发出错误。

🌐 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:

  • node18 下不允许使用 ECMAScript 模块的 require(),但在 nodenext 下允许使用
  • node18 下允许使用 import assertions(已被 import attributes 取代),但在 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 声明
  • namespacemodule 的运行时代码
  • 类中的参数属性
  • 非 ECMAScript import =export = 赋值

以下是一些无效示例:

🌐 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.json 将你的 dom 库锁定到 @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 的包,并且 TypeScript 目前在你的设置暗示 dom 时总会查找它。

🌐 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.

欲了解更多信息,请参见实现的拉取请求 see the implementing 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 生成的类型可能会影响对代码库的类型检查。 更多信息,请参见与 DOM 和此版本 TypeScript 的 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 代码”)。 它们被重新构想到一个名为import attributes的提案。 作为过渡的一部分,它们从使用 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