TypeScript 5.6

不允许进行空值和真值检查

¥Disallowed Nullish and Truthy Checks

也许你编写了一个正则表达式,但忘记调用 .test(...) 了:

¥Maybe you’ve written a regex and forgotten to call .test(...) on it:

ts
if (/0x[0-9a-f]/) {
// Oops! This block always runs.
// ...
}

或者你可能不小心把 =>(创建了一个箭头函数)写成了 >=(大于或等于运算符):

¥or maybe you’ve accidentally written => (which creates an arrow function) instead of >= (the greater-than-or-equal-to operator):

ts
if (x => 0) {
// Oops! This block always runs.
// ...
}

或者你可能尝试在 ?? 中使用默认值,但混淆了 ?? 和比较运算符(例如 <)的优先级:

¥or maybe you’ve tried to use a default value with ??, but mixed up the precedence of ?? and a comparison operator like <:

ts
function isValid(value: string | number, options: any, strictness: "strict" | "loose") {
if (strictness === "loose") {
value = +value
}
return value < options.max ?? 100;
// Oops! This is parsed as (value < options.max) ?? 100
}

或者你可能在一个复杂的表达式中放错了括号:

¥or maybe you’ve misplaced a parenthesis in a complex expression:

ts
if (
isValid(primaryValue, "strict") || isValid(secondaryValue, "strict") ||
isValid(primaryValue, "loose" || isValid(secondaryValue, "loose"))
) {
// ^^^^ 👀 Did we forget a closing ')'?
}

这些示例均未按照作者的意图执行,但它们都是有效的 JavaScript 代码。以前,TypeScript 也默默地接受了这些示例。

¥None of these examples do what the author intended, but they’re all valid JavaScript code. Previously TypeScript also quietly accepted these examples.

但是,经过一些实验,我们发现,通过标记上述可疑示例,可以捕获许多错误。在 TypeScript 5.6 中,当编译器能够从语法上确定真值或空值检查时,它会报错,并且始终会以特定方式进行求值。因此在上面的例子中,你将开始看到错误:

¥But with a little bit of experimentation, we found that many many bugs could be caught from flagging down suspicious examples like above. In TypeScript 5.6, the compiler now errors when it can syntactically determine a truthy or nullish check will always evaluate in a specific way. So in the above examples, you’ll start to see errors:

ts
if (/0x[0-9a-f]/) {
// ~~~~~~~~~~~~
// error: This kind of expression is always truthy.
}
if (x => 0) {
// ~~~~~~
// error: This kind of expression is always truthy.
}
function isValid(value: string | number, options: any, strictness: "strict" | "loose") {
if (strictness === "loose") {
value = +value
}
return value < options.max ?? 100;
// ~~~~~~~~~~~~~~~~~~~
// error: Right operand of ?? is unreachable because the left operand is never nullish.
}
if (
isValid(primaryValue, "strict") || isValid(secondaryValue, "strict") ||
isValid(primaryValue, "loose" || isValid(secondaryValue, "loose"))
) {
// ~~~~~~~
// error: This kind of expression is always truthy.
}

通过启用 ESLint no-constant-binary-expression 规则可以实现类似的结果,你可以 在他们的博客文章中查看他们取得的一些成果;但是 TypeScript 执行的新检查与 ESLint 规则并不完全重叠,我们也认为将这些检查内置到 TypeScript 本身中很有价值。

¥Similar results can be achieved by enabling the ESLint no-constant-binary-expression rule, and you can see some of the results they achieved in their blog post; but the new checks TypeScript performs does not have perfect overlap with the ESLint rule, and we also believe there is a lot of value in having these checks built into TypeScript itself.

请注意,某些表达式即使始终为真或为空,仍然是允许的。具体来说,无论 truefalse01 是否始终为真,它们仍然是允许的,因为代码如下:

¥Note that certain expressions are still allowed, even if they are always truthy or nullish. Specifically, true, false, 0, and 1 are all still allowed despite always being truthy or falsy, since code like the following:

ts
while (true) {
doStuff();
if (something()) {
break;
}
doOtherStuff();
}

仍然是惯用且有用的,并且像下面这样的代码:

¥is still idiomatic and useful, and code like the following:

ts
if (true || inDebuggingOrDevelopmentEnvironment()) {
// ...
}

在迭代/调试代码时很有用。

¥is useful while iterating/debugging code.

如果你对它的实现或它捕获的错误类型感到好奇,请查看 实现此功能的拉取请求

¥If you’re curious about the implementation or the sorts of bugs it catches, take a look at the pull request that implemented this feature.

迭代器辅助方法

¥Iterator Helper Methods

JavaScript 有可迭代对象(可以通过调用 [Symbol.iterator]() 并获取迭代器进行迭代的对象)和迭代器(具有 next() 方法的对象,我们可以在迭代时调用该方法尝试获取下一个值)。总的来说,当你将它们放入 for/of 循环中,或将它们 [...spread] 到新数组中时,通常不必考虑这些事情。但是 TypeScript 确实使用类型 IterableIterator(甚至 IterableIterator 也可以同时充当这两种类型!)来建模这些类型,并且这些类型描述了像 for/of 这样的构造在其上工作所需的最小成员集。

¥JavaScript has a notion of iterables (things which we can iterate over by calling a [Symbol.iterator]() and getting an iterator) and iterators (things which have a next() method which we can call to try to get the next value as we iterate). By and large, you don’t typically have to think about these things when you toss them into a for/of loop, or [...spread] them into a new array. But TypeScript does model these with the types Iterable and Iterator (and even IterableIterator which acts as both!), and these types describe the minimal set of members you need for constructs like for/of to work on them.

Iterable(和 IterableIterator)很棒,因为它们可以在 JavaScript 的各种地方使用。 - 但很多人发现自己缺少 Array 上的方法,例如 mapfilter,以及出于某种原因缺少 reduce。这就是 ECMAScript 中最近提出了一项提案 可以将许多(甚至更多)来自 Array 的方法添加到 JavaScript 生成的大多数 IterableIterator 中的原因。

¥Iterables (and IterableIterators) are nice because they can be used in all sorts of places in JavaScript - but a lot of people found themselves missing methods on Arrays like map, filter, and for some reason reduce. That’s why a recent proposal was brought forward in ECMAScript to add many methods (and more) from Array onto most of the IterableIterators that are produced in JavaScript.

例如,现在每个生成器都会生成一个对象,该对象也包含 map 方法和 take 方法。

¥For example, every generator now produces an object that also has a map method and a take method.

ts
function* positiveIntegers() {
let i = 1;
while (true) {
yield i;
i++;
}
}
const evenNumbers = positiveIntegers().map(x => x * 2);
// Output:
// 2
// 4
// 6
// 8
// 10
for (const value of evenNumbers.take(5)) {
console.log(value);
}

MapSet 上的 keys()values()entries() 等方法也是如此。

¥The same is true for methods like keys(), values(), and entries() on Maps and Sets.

ts
function invertKeysAndValues<K, V>(map: Map<K, V>): Map<V, K> {
return new Map(
map.entries().map(([k, v]) => [v, k])
);
}

你还可以扩展新的 Iterator 对象:

¥You can also extend the new Iterator object:

ts
/**
* Provides an endless stream of `0`s.
*/
class Zeroes extends Iterator<number> {
next() {
return { value: 0, done: false } as const;
}
}
const zeroes = new Zeroes();
// Transform into an endless stream of `1`s.
const ones = zeroes.map(x => x + 1);

你可以使用 Iterator.from 将任何现有的 IterableIterator 适配到这个新类型中:

¥And you can adapt any existing Iterables or Iterators into this new type with Iterator.from:

ts
Iterator.from(...).filter(someFunction);

现在,我们必须讨论命名。

¥Now, we have to talk about naming.

之前我们提到,TypeScript 拥有 IterableIterator 的类型;但是,正如我们提到的,它们的作用类似于 “protocols”,以确保某些操作能够正常工作。这意味着并非每个在 TypeScript 中声明为 IterableIterator 的值都具有我们上面提到的方法。

¥Earlier we mentioned that TypeScript has types for Iterable and Iterator; however, like we mentioned, these act sort of like “protocols” to ensure certain operations work. That means that not every value that is declared Iterable or Iterator in TypeScript will have those methods we mentioned above.

但是仍然有一个名为 Iterator 的新运行时值。你可以将 IteratorIterator.prototype 引用为 JavaScript 中的实际值。这有点尴尬,因为 TypeScript 已经定义了一个名为 Iterator 的东西,纯粹用于类型检查。由于这个不幸的名称冲突,TypeScript 需要引入一个单独的类型来描述这些原生/内置可迭代器。

¥But there is still a new runtime value called Iterator. You can reference Iterator, as well as Iterator.prototype, as actual values in JavaScript. This is a bit awkward since TypeScript already defines its own thing called Iterator purely for type-checking. So due to this unfortunate name clash, TypeScript needs to introduce a separate type to describe these native/built-in iterable iterators.

TypeScript 5.6 引入了一种名为 IteratorObject 的新类型。它的定义如下:

¥TypeScript 5.6 introduces a new type called IteratorObject. It is defined as follows:

ts
interface IteratorObject<T, TReturn = unknown, TNext = unknown> extends Iterator<T, TReturn, TNext> {
[Symbol.iterator](): IteratorObject<T, TReturn, TNext>;
}

许多内置集合和方法都会生成 IteratorObject 的子类型(例如 ArrayIteratorSetIteratorMapIterator 等),并且 lib.d.ts@types/node 中的核心 JavaScript 和 DOM 类型都已更新为使用此新类型。

¥Lots of built-in collections and methods produce subtypes of IteratorObjects (like ArrayIterator, SetIterator, MapIterator, and more), and both the core JavaScript and DOM types in lib.d.ts, along with @types/node, have been updated to use this new type.

同样,奇偶校验有一个 AsyncIteratorObject 类型。AsyncIterator 尚不存在 JavaScript 中的运行时值,它为 AsyncIterable但这是一个有效的提案 提供相同的方法,并且此新类型为此做好了准备。

¥Similarly, there is a AsyncIteratorObject type for parity. AsyncIterator does not yet exist as a runtime value in JavaScript that brings the same methods for AsyncIterables, but it is an active proposal and this new type prepares for it.

我们要感谢 Kevin Gibbons这些类型的更改 的贡献者之一 Kevin Gibbons

¥We’d like to thank Kevin Gibbons who contributed the changes for these types, and who is one of the co-authors of the proposal.

严格的内置迭代器检查(和 --strictBuiltinIteratorReturn

¥Strict Builtin Iterator Checks (and --strictBuiltinIteratorReturn)

Iterator<T, TReturn> 上调用 next() 方法时,它会返回一个具有 valuedone 属性的对象。这由类型 IteratorResult 建模。

¥When you call the next() method on an Iterator<T, TReturn>, it returns an object with a value and a done property. This is modeled with the type IteratorResult.

ts
type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;
interface IteratorYieldResult<TYield> {
done?: false;
value: TYield;
}
interface IteratorReturnResult<TReturn> {
done: true;
value: TReturn;
}

此处的命名方式受生成器函数工作方式的启发。生成器函数可以 yield 为值,然后 return 为最终值。 - 但是两者之间的类型可以不相关。

¥The naming here is inspired by the way a generator function works. Generator functions can yield values, and then return a final value - but the types between the two can be unrelated.

ts
function abc123() {
yield "a";
yield "b";
yield "c";
return 123;
}
const iter = abc123();
iter.next(); // { value: "a", done: false }
iter.next(); // { value: "b", done: false }
iter.next(); // { value: "c", done: false }
iter.next(); // { value: 123, done: true }

使用新的 IteratorObject 类型,我们发现在安全实现 IteratorObject 方面存在一些困难。同时,在 TReturnany(默认值!)的情况下,使用 IteratorResult 长期以来一直存在不安全性。例如,假设我们有一个 IteratorResult<string, any> 函数。如果我们最终找到这种类型的 value,我们最终会得到 string | any,而 string | any 就是 any

¥With the new IteratorObject type, we discovered some difficulties in allowing safe implementations of IteratorObjects. At the same time, there’s been a long standing unsafety with IteratorResult in cases where TReturn was any (the default!). For example, let’s say we have an IteratorResult<string, any>. If we end up reaching for the value of this type, we’ll end up with string | any, which is just any.

ts
function* uppercase(iter: Iterator<string, any>) {
while (true) {
const { value, done } = iter.next();
yield value.toUppercase(); // oops! forgot to check for `done` first and misspelled `toUpperCase`
if (done) {
return;
}
}
}

现在,如果不引入大量中断,很难在每个 Iterator 上修复这个问题,但我们至少可以在大多数创建的 IteratorObject 上修复它。

¥It would be hard to fix this on every Iterator today without introducing a lot of breaks, but we can at least fix it with most IteratorObjects that get created.

TypeScript 5.6 引入了一种名为 BuiltinIteratorReturn 的新内在类型和一个名为 --strictBuiltinIteratorReturn 的新 --strict 模式标志。每当在 lib.d.ts 等地方使用 IteratorObject 时,它们总是用 BuiltinIteratorReturn 类型表示 TReturn(尽管你会更经常看到更具体的 MapIteratorArrayIteratorSetIterator)。

¥TypeScript 5.6 introduces a new intrinsic type called BuiltinIteratorReturn and a new --strict-mode flag called --strictBuiltinIteratorReturn. Whenever IteratorObjects are used in places like lib.d.ts, they are always written with BuiltinIteratorReturn type for TReturn (though you’ll see the more-specific MapIterator, ArrayIterator, SetIterator more often).

ts
interface MapIterator<T> extends IteratorObject<T, BuiltinIteratorReturn, unknown> {
[Symbol.iterator](): MapIterator<T>;
}
// ...
interface Map<K, V> {
// ...
/**
* Returns an iterable of key, value pairs for every entry in the map.
*/
entries(): MapIterator<[K, V]>;
/**
* Returns an iterable of keys in the map
*/
keys(): MapIterator<K>;
/**
* Returns an iterable of values in the map
*/
values(): MapIterator<V>;
}

默认情况下,BuiltinIteratorReturnany,但当启用 --strictBuiltinIteratorReturn(可能通过 --strict)时,它是 undefined。在此新模式下,如果我们使用 BuiltinIteratorReturn,我们之前的示例现在可以正确出错:

¥By default, BuiltinIteratorReturn is any, but when --strictBuiltinIteratorReturn is enabled (possibly via --strict), it is undefined. Under this new mode, if we use BuiltinIteratorReturn, our earlier example now correctly errors:

ts
function* uppercase(iter: Iterator<string, BuiltinIteratorReturn>) {
while (true) {
const { value, done } = iter.next();
yield value.toUppercase();
// ~~~~~ ~~~~~~~~~~~
// error! ┃ ┃
// ┃ ┗━ Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
// ┃
// ┗━ 'value' is possibly 'undefined'.
if (done) {
return;
}
}
}

lib.d.ts 中,你通常会看到 BuiltinIteratorReturnIteratorObject 配对。一般来说,我们建议尽可能在你自己的代码中更明确地说明 TReturn

¥You’ll typically see BuiltinIteratorReturn paired up with IteratorObject throughout lib.d.ts. In general, we recommend being more explicit around the TReturn in your own code when possible.

更多信息,你可以查看 在此处阅读功能信息

¥For more information, you can read up on the feature here.

支持任意模块标识符

¥Support for Arbitrary Module Identifiers

JavaScript 允许模块将具有无效标识符名称的绑定导出为字符串字面量:

¥JavaScript allows modules to export bindings with invalid identifier names as string literals:

ts
const banana = "🍌";
export { banana as "🍌" };

同样,它允许模块获取具有以下任意名称的导入,并将它们绑定到有效的标识符:

¥Likewise, it allows modules to grab imports with these arbitrary names and bind them to valid identifiers:

ts
import { "🍌" as banana } from "./foo"
/**
* om nom nom
*/
function eat(food: string) {
console.log("Eating", food);
};
eat(banana);

这看起来像是一个可爱的派对小技巧(如果你在派对上和我们一样有趣的话),但它在与其他语言的互操作性方面也有用处(通常通过 JavaScript/WebAssembly 边界),因为其他语言对有效标识符的构成可能有不同的规则。它对于生成代码的工具(例如 esbuild inject 功能)也很有用。

¥This seems like a cute party trick (if you’re as fun as we are at parties), but it has its uses for interoperability with other languages (typically via JavaScript/WebAssembly boundaries), since other languages may have different rules for what constitutes a valid identifier. It can also be useful for tools that generate code, like esbuild with its inject feature.

TypeScript 5.6 现在允许你在代码中使用这些任意模块标识符!我们要感谢 Evan Wallace将此更改贡献给 TypeScript

¥TypeScript 5.6 now allows you to use these arbitrary module identifiers in your code! We’d like to thank Evan Wallace who contributed this change to TypeScript!

--noUncheckedSideEffectImports 选项

¥The --noUncheckedSideEffectImports Option

在 JavaScript 中,可以 import 模块而无需实际从中导入任何值。

¥In JavaScript it’s possible to import a module without actually importing any values from it.

ts
import "some-module";

这些导入通常被称为副作用导入,因为它们可以提供的唯一有用的行为是执行一些副作用(例如注册全局变量或向原型添加 polyfill)。

¥These imports are often called side effect imports because the only useful behavior they can provide is by executing some side effect (like registering a global variable, or adding a polyfill to a prototype).

在 TypeScript 中,此语法有一个非常奇怪的怪癖:如果 import 可以解析为有效的源文件,则 TypeScript 会加载并检查该文件。另一方面,如果找不到源文件,TypeScript 会默默忽略 import

¥In TypeScript, this syntax has had a pretty strange quirk: if the import could be resolved to a valid source file, then TypeScript would load and check the file. On the other hand, if no source file could be found, TypeScript would silently ignore the import!

这是一种令人惊讶的行为,但它部分源于 JavaScript 生态系统中的建模模式。例如,此语法还已与打包器中的特殊加载器一起使用,以加载 CSS 或其他资源。你的打包器可能配置为你可以通过编写类似以下内容来包含特定的 .css 文件:

¥This is surprising behavior, but it partially stems from modeling patterns in the JavaScript ecosystem. For example, this syntax has also been used with special loaders in bundlers to load CSS or other assets. Your bundler might be configured in such a way where you can include specific .css files by writing something like the following:

tsx
import "./button-component.css";
export function Button() {
// ...
}

不过,这掩盖了副作用导入中的潜在拼写错误。这就是为什么 TypeScript 5.6 引入了一个名为 --noUncheckedSideEffectImports 的新编译器选项,以捕获这些情况。启用 --noUncheckedSideEffectImports 后,如果 TypeScript 找不到副作用导入的源文件,现在将出错。

¥Still, this masks potential typos on side effect imports. That’s why TypeScript 5.6 introduces a new compiler option called --noUncheckedSideEffectImports, to catch these cases. When --noUncheckedSideEffectImports is enabled, TypeScript will now error if it can’t find a source file for a side effect import.

ts
import "oops-this-module-does-not-exist";
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// error: Cannot find module 'oops-this-module-does-not-exist' or its corresponding type declarations.

启用此选项后,某些工作代码现在可能会收到错误,如上面的 CSS 示例。为了解决这个问题,只想为资源编写副作用 import 的用户可能最好通过编写带有通配符说明符的环境模块声明来提供服务。它将进入全局文件并看起来像下面这样:

¥When enabling this option, some working code may now receive an error, like in the CSS example above. To work around this, users who want to just write side effect imports for assets might be better served by writing what’s called an ambient module declaration with a wildcard specifier. It would go in a global file and look something like the following:

ts
// ./src/globals.d.ts
// Recognize all CSS files as module imports.
declare module "*.css" {}

事实上,你的项目中可能已经有这样的文件了!例如,运行类似 vite init 的程序可能会创建类似的 vite-env.d.ts

¥In fact, you might already have a file like this in your project! For example, running something like vite init might create a similar vite-env.d.ts.

虽然此选项目前默认关闭,但我们鼓励用户尝试一下!

¥While this option is currently off by default, we encourage users to give it a try!

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

¥For more information, check out the implementation here.

--noCheck 选项

¥The --noCheck Option

TypeScript 5.6 引入了一个新的编译器选项 --noCheck,它允许你跳过所有输入文件的类型检查。这避免了在执行生成输出文件所需的任何语义分析时进行不必要的类型检查。

¥TypeScript 5.6 introduces a new compiler option, --noCheck, which allows you to skip type checking for all input files. This avoids unnecessary type-checking when performing any semantic analysis necessary for emitting output files.

一种方案是将 JavaScript 文件生成与类型检查分开,以便将两者作为单独的阶段运行。例如,你可以在迭代时运行 tsc --noCheck,然后运行 ​​tsc --noEmit 进行彻底的类型检查。即使在 --watch 模式下,你也可以并行运行这两个任务,但请注意,如果你确实同时运行它们,你可能需要指定单独的 --tsBuildInfoFile 路径。

¥One scenario for this is to separate JavaScript file generation from type-checking so that the two can be run as separate phases. For example, you could run tsc --noCheck while iterating, and then tsc --noEmit for a thorough type check. You could also run the two tasks in parallel, even in --watch mode, though note you’d probably want to specify a separate --tsBuildInfoFile path if you’re truly running them at the same time.

--noCheck 也可用于以类似的方式生成声明文件。在符合 --isolatedDeclarations 的项目上指定 --noCheck 的项目中,TypeScript 可以快速生成声明文件,而无需进行类型检查。生成的声明文件将完全依赖于快速的语法转换。

¥--noCheck is also useful for emitting declaration files in a similar fashion. In a project where --noCheck is specified on a project that conforms to --isolatedDeclarations, TypeScript can quickly generate declaration files without a type-checking pass. The generated declaration files will rely purely on quick syntactic transformations.

请注意,如果指定了 --noCheck,但项目未使用 --isolatedDeclarations,TypeScript 仍会执行必要的类型检查来生成 .d.ts 文件。从这个意义上讲,“--noCheck”这个名称有点用词不当;但是,该过程比完整的类型检查更懒惰,只计算未注释声明的类型。这应该比完整的类型检查快得多。

¥Note that in cases where --noCheck is specified, but a project does not use --isolatedDeclarations, TypeScript may still perform as much type-checking as necessary to generate .d.ts files. In this sense, --noCheck is a bit of a misnomer; however, the process will be lazier than a full type-check, only calculating the types of unannotated declarations. This should be much faster than a full type-check.

noCheck 也可以通过 TypeScript API 作为标准选项使用。在内部,transpileModuletranspileDeclaration 已经使用 noCheck 来加速(至少从 TypeScript 5.5 开始)。现在,任何构建工具都应该能够利用该标志,并采取各种自定义策略来协调和加速构建。

¥noCheck is also available via the TypeScript API as a standard option. Internally, transpileModule and transpileDeclaration already used noCheck to speed things up (at least as of TypeScript 5.5). Now any build tool should be able to leverage the flag, taking a variety of custom strategies to coordinate and speed up builds.

更多信息,请参阅 在 TypeScript 5.5 中为内部增强 noCheck 所做的工作,以及将其公开发布的 关于命令行 和相关工作

¥For more information, see the work done in TypeScript 5.5 to power up noCheck internally, along with the relevant work to make it publicly available on the command line and

允许 --build 出现中间错误

¥Allow --build with Intermediate Errors

TypeScript 的项目引用概念允许你将代码库组织到多个项目中,并在它们之间创建依赖。在 --build 模式(简称 tsc -b)下运行 TypeScript 编译器是跨项目进行构建并确定哪些项目和文件需要编译的内置方法。

¥TypeScript’s concept of project references allows you to organize your codebase into multiple projects and create dependencies between them. Running the TypeScript compiler in --build mode (or tsc -b for short) is the built-in way of actually conducting that build across projects and figuring out which projects and files need to be compiled.

以前,使用 --build 模式会假定 --noEmitOnError,并在遇到任何错误时立即停止构建。这意味着如果 “downstream” 项目的任何 “upstream” 依赖存在构建错误,则永远无法检查和构建 “downstream” 项目。理论上,这是一种非常有效的方法 - 如果项目出现错误,则其依赖不一定处于一致状态。

¥Previously, using --build mode would assume --noEmitOnError and immediately stop the build if any errors were encountered. This meant that “downstream” projects could never be checked and built if any of their “upstream” dependencies had build errors. In theory, this is a very cromulent approach - if a project has errors, it is not necessarily in a coherent state for its dependencies.

实际上,这种僵化使得升级之类的事情变得非常麻烦。例如,如果 projectB 依赖于 projectA,那么熟悉 projectB 的人就无法主动升级其代码,直到其依赖升级为止。由于首先要升级 projectA,所以它们被阻止了。

¥In reality, this sort of rigidity made things like upgrades a pain. For example, if projectB depends on projectA, then people more familiar with projectB can’t proactively upgrade their code until their dependencies are upgraded. They are blocked by work on upgrading projectA first.

从 TypeScript 5.6 开始,即使依赖中存在中间错误,--build 模式也将继续构建项目。如果遇到中间错误,系统会一致地报告错误,并尽力生成输出文件;但是,构建将继续在指定项目上完成。

¥As of TypeScript 5.6, --build mode will continue to build projects even if there are intermediate errors in dependencies. In the face of intermediate errors, they will be reported consistently and output files will be generated on a best-effort basis; however, the build will continue to completion on the specified project.

如果你想在第一个出现错误的项目上停止构建,可以使用名为 --stopOnBuildErrors 的新标志。在持续集成 (CI) 环境中运行,或者在迭代一个被其他项目严重依赖的项目时,这会很有用。

¥If you want to stop the build on the first project with errors, you can use a new flag called --stopOnBuildErrors. This can be useful when running in a CI environment, or when iterating on a project that’s heavily depended upon by other projects.

请注意,为了实现这一点,TypeScript 现在始终会在 --build 调用中为任何项目生成一个 .tsbuildinfo 文件(即使未指定 --incremental/--composite)。这是为了跟踪 --build 的调用状态以及将来需要执行的工作。

¥Note that to accomplish this, TypeScript now always emits a .tsbuildinfo file for any project in a --build invocation (even if --incremental/--composite is not specified). This is to keep track of the state of how --build was invoked and what work needs to be performed in the future.

你可以 在此处阅读有关此变更的更多信息

¥You can read more about this change here on the implementation.

编辑器中的区域优先诊断

¥Region-Prioritized Diagnostics in Editors

当 TypeScript 的语言服务被要求对文件进行诊断(例如错误、建议和弃用)时,通常需要检查整个文件。大多数情况下,这样做没问题,但在非常大的文件中可能会导致延迟。这可能会令人沮丧,因为修复拼写错误应该感觉像是一个快速的操作,但在足够大的文件中可能需要几秒钟。

¥When TypeScript’s language service is asked for the diagnostics for a file (things like errors, suggestions, and deprecations), it would typically require checking the entire file. Most of the time this is fine, but in extremely large files it can incur a delay. That can be frustrating because fixing a typo should feel like a quick operation, but can take seconds in a big-enough file.

为了解决这个问题,TypeScript 5.6 引入了一项名为区域优先诊断或区域优先检查的新功能。编辑器现在不仅可以请求一组文件的诊断信息,还可以提供给定文件的相关区域。 - 其目的是,这通常是文件中当前对用户可见的区域。TypeScript 语言服务器可以选择提供两组诊断信息:一个用于区域,一个用于整个文件。这使得在大文件中编辑时响应速度更快,你无需等待红色波浪线消失太久。

¥To address this, TypeScript 5.6 introduces a new feature called region-prioritized diagnostics or region-prioritized checking. Instead of just requesting diagnostics for a set of files, editors can now also provide a relevant region of a given file - and the intent is that this will typically be the region of the file that is currently visible to a user. The TypeScript language server can then choose to provide two sets of diagnostics: one for the region, and one for the file in its entirety. This allows editing to feel way more responsive in large files so you’re not waiting as long for thoes red squiggles to disappear.

对于一些特定的数字,在我们的测试 关于 TypeScript 自身的 checker.ts 中,完整的语义诊断响应耗时 3330 毫秒。相比之下,第一个基于区域的诊断响应耗时 143 毫秒!虽然剩余的全文件响应大约需要 3200 毫秒,但这对于快速编辑来说意义重大。

¥For some specific numbers, in our testing on TypeScript’s own checker.ts, a full semantic diagnostics response took 3330ms. In contrast, the response for the first region-based diagnostics response took 143ms! While the remaining whole-file response took about 3200ms, this can make a huge difference for quick edits.

此功能还包含大量工作,以使诊断报告在整个体验过程中更加一致。由于我们的类型检查器利用缓存来避免重复工作,因此在相同类型之间进行后续检查通常会显示不同的(通常更短的)错误消息。从技术上讲,惰性乱序检查可能会导致编辑器中两个位置的诊断报告不同。 - 甚至在此功能之前 - 但我们不想让问题进一步恶化。通过最近的工作,我们已经解决了许多此类错误不一致问题。

¥This feature also includes quite a bit of work to also make diagnostics report more consistently throughout your experience. Due the way our type-checker leverages caching to avoid work, subsequent checks between the same types could often have a different (typically shorter) error message. Technically, lazy out-of-order checking could cause diagnostics to report differently between two locations in an editor - even before this feature - but we didn’t want to exacerbate the issue. With recent work, we’ve ironed out many of these error inconsistencies.

目前,此功能在 Visual Studio Code 中适用于 TypeScript 5.6 及更高版本。

¥Currently, this functionality is available in Visual Studio Code for TypeScript 5.6 and later.

更多详细信息,请访问 查看此处的实现和编写

¥For more detailed information, take a look at the implementation and write-up here.

细粒度提交字符

¥Granular Commit Characters

TypeScript 的语言服务现在为每个完成项提供自己的提交字符。提交字符是特定的字符,输入后将自动提交当前建议的完成项。

¥TypeScript’s language service now provides its own commit characters for each completion item. Commit characters are specific characters that, when typed, will automatically commit the currently-suggested completion item.

这意味着随着时间的推移,当你输入某些字符时,你的编辑器现在将更频繁地提交当前建议的补全项。例如,采用以下代码:

¥What this means is that over time your editor will now more frequently commit to the currently-suggested completion item when you type certain characters. For example, take the following code:

ts
declare let food: {
eat(): any;
}
let f = (foo/**/

如果我们的光标位于 /**/,我们不清楚我们正在编写的代码是否会像 let f = (food.eat())let f = (foo, bar) => foo + bar 那样。你可以想象,编辑器可能会根据我们接下来输入的字符自动补齐不同的功能。例如,如果我们输入句点字符 (.),我们可能希望编辑器使用变量 food 完成;但是如果我们输入逗号 (,),我们可能会在箭头函数中写出一个参数。

¥If our cursor is at /**/, it’s unclear if the code we’re writing is going to be something like let f = (food.eat()) or let f = (foo, bar) => foo + bar. You could imagine that the editor might be able to auto-complete differently depending on which character we type out next. For instance, if we type in the period/dot character (.), we probably want the editor to complete with the variable food; but if we type the comma character (,), we might be writing out a parameter in an arrow function.

遗憾的是,之前 TypeScript 只是向编辑器触发信号,表示当前文本可能定义了一个新的参数名称,因此任何提交字符都不安全。因此,即使编辑器应该使用单词 food 自动补齐的是 “明确的”,输入 . 也不会执行任何操作。

¥Unfortunately, previously TypeScript just signaled to editors that the current text might define a new parameter name so that no commit characters were safe. So hitting a . wouldn’t do anything even if it was “obvious” that the editor should auto-complete with the word food.

TypeScript 现在会明确列出每个补全项可以安全提交的字符。虽然这不会立即改变你的日常体验,但支持这些提交字符的编辑器应该会随着时间的推移看到行为上的改进。要立即查看这些改进,你现在可以将 使用 TypeScript Nightly 扩展Visual Studio Code 内部人员 结合使用。在上面的代码中,正确地点击 . 会自动补全为 food

¥TypeScript now explicitly lists which characters are safe to commit for each completion item. While this won’t immediately change your day-to-day experience, editors that support these commit characters should see behavioral improvements over time. To see those improvements right now, you can now use the TypeScript nightly extension with Visual Studio Code Insiders. Hitting . in the code above correctly auto-completes with food.

更多信息,请参阅 添加提交字符的拉取请求 以及我们的 根据上下文调整提交字符

¥For more information, see the pull request that added commit characters along with our adjustments to commit characters depending on context.

自动导入的排除模式

¥Exclude Patterns for Auto-Imports

TypeScript 的语言服务现在允许你指定一个正则表达式模式列表,该列表将从某些说明符中过滤掉自动导入建议。例如,如果你想从像 lodash 这样的包中排除所有 “deep” 导入,你可以在 Visual Studio Code 中配置以下首选项:

¥TypeScript’s language service now allows you to specify a list of regular expression patterns which will filter away auto-import suggestions from certain specifiers. For example, if you want to exclude all “deep” imports from a package like lodash, you could configure the following preference in Visual Studio Code:

json
{
"typescript.preferences.autoImportSpecifierExcludeRegexes": [
"^lodash/.*$"
]
}

或者反过来,你可能希望禁止从包的入口点导入:

¥Or going the other way, you might want to disallow importing from the entry-point of a package:

json
{
"typescript.preferences.autoImportSpecifierExcludeRegexes": [
"^lodash$"
]
}

甚至可以使用以下设置来避免 node: 导入:

¥One could even avoid node: imports by using the following setting:

json
{
"typescript.preferences.autoImportSpecifierExcludeRegexes": [
"^node:"
]
}

请注意,如果要指定某些标志,例如 iu,则需要用斜杠将正则表达式括起来。在提供周围的斜杠时,需要转义其他内部斜杠。

¥Note that if you want to specify certain flags like i or u, you will need to surround your regular expression with slashes. When providing surrounding slashes, you’ll need to escape other inner slashes.

json
{
"typescript.preferences.autoImportSpecifierExcludeRegexes": [
"^./lib/internal", // no escaping needed
"/^.\\/lib\\/internal/", // escaping needed - note the leading and trailing slashes
"/^.\\/lib\\/internal/i" // escaping needed - we needed slashes to provide the 'i' regex flag
]
}

在 Visual Studio Code 中,可以通过 javascript.preferences.autoImportSpecifierExcludeRegexes 对 JavaScript 应用相同的设置。

¥In Visual Studio Code, the same settings can be applied for JavaScript through javascript.preferences.autoImportSpecifierExcludeRegexes.

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

¥For more information, see the implementation here.

值得注意的行为变更

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

.tsbuildinfo 始终写入

¥.tsbuildinfo is Always Written

为了使 --build 即使在依赖中存在中间错误的情况下也能继续构建项目,并支持在命令行上支持 --noCheck,TypeScript 现在始终会在 --build 调用中为任何项目生成一个 .tsbuildinfo 文件。无论 --incremental 是否实际启用,都会发生这种情况。查看此处更多信息

¥To enable --build to continue building projects even if there are intermediate errors in dependencies, and to support --noCheck on the command line, TypeScript now always emits a .tsbuildinfo file for any project in a --build invocation. This happens regardless of whether --incremental is actually on. See more information here.

node_modules 中遵循文件扩展名和 package.json

¥Respecting File Extensions and package.json from within node_modules

在 Node.js 在 v12 中实现对 ECMAScript 模块的支持之前,TypeScript 一直无法确定它在 node_modules 中找到的 .d.ts 文件是 CommonJS 还是 ECMAScript 模块编写的 JavaScript 文件。当 npm 的绝大部分版本都只支持 CommonJS 时,这并没有造成太多问题。 - 如果有疑问,TypeScript 可以假设所有内容的行为都与 CommonJS 类似。遗憾的是,如果这个假设错误,它可能会允许不安全的导入:

¥Before Node.js implemented support for ECMAScript modules in v12, there was never a good way for TypeScript to know whether .d.ts files it found in node_modules represented JavaScript files authored as CommonJS or ECMAScript modules. When the vast majority of npm was CommonJS-only, this didn’t cause many problems - if in doubt, TypeScript could just assume that everything behaved like CommonJS. Unfortunately, if that assumption was wrong it could allow unsafe imports:

ts
// node_modules/dep/index.d.ts
export declare function doSomething(): void;
// index.ts
// Okay if "dep" is a CommonJS module, but fails if
// it's an ECMAScript module - even in bundlers!
import dep from "dep";
dep.doSomething();

实际上,这种情况并不常见。但自从 Node.js 开始支持 ECMAScript 模块以来,ESM 在 npm 上的份额有所增长。幸运的是,Node.js 还引入了一种机制,可以帮助 TypeScript 判断文件是 ECMAScript 模块还是 CommonJS 模块:.mjs.cjs 文件扩展名以及 package.json "type" 字段。TypeScript 4.7 增加了对理解这些指示符以及编写 .mts.cts 文件的支持;然而,TypeScript 只会读取 --module node16--module nodenext 下的指示符,因此对于使用 --module esnext--moduleResolution bundler 的用户来说,上述不安全导入仍然是个问题。

¥In practice, this didn’t come up very often. But in the years since Node.js started supporting ECMAScript modules, the share of ESM on npm has grown. Fortunately, Node.js also introduced a mechanism that can help TypeScript determine if a file is an ECMAScript module or a CommonJS module: the .mjs and .cjs file extensions and the package.json "type" field. TypeScript 4.7 added support for understanding these indicators, as well as authoring .mts and .cts files; however, TypeScript would only read those indicators under --module node16 and --module nodenext, so the unsafe import above was still a problem for anyone using --module esnext and --moduleResolution bundler, for example.

为了解决这个问题,TypeScript 5.6 收集模块格式信息,并使用它来解决所有 module 模式(amdumdsystem 除外)中类似上述示例中的歧义。特定格式的文件扩展名(.mts.cts)在任何地方都会被遵循,并且无论 module 的设置如何,node_modules 依赖中都会查询 package.json "type" 字段。以前,从技术上来说,可以将 CommonJS 输出生成到 .mjs 文件,反之亦然:

¥To solve this, TypeScript 5.6 collects module format information and uses it to resolve ambiguities like the one in the example above in all module modes (except amd, umd, and system). Format-specific file extensions (.mts and .cts) are respected anywhere they’re found, and the package.json "type" field is consulted inside node_modules dependencies, regardless of the module setting. Previously, it was technically possible to produce CommonJS output into a .mjs file or vice versa:

ts
// main.mts
export default "oops";
// $ tsc --module commonjs main.mts
// main.mjs
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = "oops";

现在,.mts 文件永远不会输出 CommonJS 输出,.cts 文件也不会输出 ESM 输出。

¥Now, .mts files never emit CommonJS output, and .cts files never emit ESM output.

请注意,这种行为大部分是在 TypeScript 5.5(实现细节在这里)的预发布版本中提供的,但在 5.6 中,这种行为仅扩展到 node_modules 中的文件。

¥Note that much of this behavior was provided in pre-release versions of TypeScript 5.5 (implementation details here), but in 5.6 this behavior is only extended to files within node_modules.

更多详细信息请参见 关于此变更,请点击此处

¥More details are available on the change here.

更正计算属性的 override 检查

¥Correct override Checks on Computed Properties

以前,标有 override 的计算属性无法正确检查基类成员是否存在。同样,如果你使用了 noImplicitOverride,即使你忘记在计算属性中添加 override 修饰符,也不会收到错误。

¥Previously, computed properties marked with override did not correctly check for the existence of a base class member. Similarly, if you used noImplicitOverride, you would not get an error if you forgot to add an override modifier to a computed property.

TypeScript 5.6 现在可以在两种情况下正确检查计算属性。

¥TypeScript 5.6 now correctly checks computed properties in both cases.

ts
const foo = Symbol("foo");
const bar = Symbol("bar");
class Base {
[bar]() {}
}
class Derived extends Base {
override [foo]() {}
// ~~~~~
// error: This member cannot have an 'override' modifier because it is not declared in the base class 'Base'.
[bar]() {}
// ~~~~~
// error under noImplicitOverride: This member must have an 'override' modifier because it overrides a member in the base class 'Base'.
}

此修复得益于 此拉取请求 中的 Oleksandr Tarasiuk

¥This fix was contributed thanks to Oleksandr Tarasiuk in this pull request.