返回表达式中分支的细粒度检查
¥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
这样的条件表达式时,它的类型被视为两个分支类型的联合。换句话说,它会获取 trueBranch
和 falseBranch
的类型,并将它们组合成一个联合类型。在这种情况下,untypedCache.get(urlString)
的类型是 any
,urlString
的类型是 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 undernode18
, but allowed undernodenext
-
导入断言(已弃用,建议使用导入属性)在
node18
下可用,但在nodenext
下不可用。¥import assertions (deprecated in favor of import attributes) are allowed under
node18
, but are disallowed undernodenext
查看 --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 -
带有运行时代码的
namespace
和module
¥
namespace
s andmodule
s with runtime code -
类中的参数属性
¥parameter properties in classes
-
非 ECMAScript
import =
和export =
分配¥Non-ECMAScript
import =
andexport =
assignments
以下是一些无效示例:
¥Here are some examples of what does not work:
ts
// ❌ error: An `import ... = require(...)` aliasimport foo = require("foo");// ❌ error: A namespace with runtime code.namespace container {}// ❌ error: An `import =` aliasimport Bar = container.Bar;class Point {// ❌ error: Parameter propertiesconstructor(public x: number, public y: number) { }}// ❌ error: An `export =` assignment.export = Point;// ❌ error: An enum declaration.enum Direction {Up,Down,Left,Right,}
类似工具如 ts-blank-space 或 Amaro(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
的包,并且当你的设置暗示使用 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 将在类的计算属性名称中始终保留实体名称(bareVariables
和 dotted.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 symbol
s 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