TypeScript 5.6
禁止的空值和真值检查
也许你编写了一个正则表达式,却忘记对其调用 .test(...):
if (/0x[0-9a-f]/) {
// 哎呀!这个块总是会运行。
// ...
}或者你可能不小心写了 =>(这会创建一个箭头函数)而不是 >=(大于或等于运算符):
if (x => 0) {
// 哎呀!这个块总是会运行。
// ...
}或者你可能尝试使用 ?? 设置默认值,但混淆了 ?? 和比较运算符(如 <)的优先级:
function isValid(value: string | number, options: any, strictness: "strict" | "loose") {
if (strictness === "loose") {
value = +value
}
return value < options.max ?? 100;
// 哎呀!这被解析为 (value < options.max) ?? 100
}或者你可能在复杂表达式中放错了括号:
if (
isValid(primaryValue, "strict") || isValid(secondaryValue, "strict") ||
isValid(primaryValue, "loose" || isValid(secondaryValue, "loose"))
) {
// ^^^^ 👀 我们忘记了一个闭合的 ')' 吗?
}这些示例都没有达到作者的预期,但它们都是有效的 JavaScript 代码。 以前 TypeScript 也静默接受了这些示例。
但经过一些实验,我们发现通过标记出像上面这样的可疑示例,可以捕获很多很多 bug。 在 TypeScript 5.6 中,当编译器可以在语法上确定一个真值或空值检查总是以特定方式求值时,现在会报错。 因此,在上述示例中,你将开始看到错误:
if (/0x[0-9a-f]/) {
// ~~~~~~~~~~~~
// 错误:这种表达式总是真值。
}
if (x => 0) {
// ~~~~~~
// 错误:这种表达式总是真值。
}
function isValid(value: string | number, options: any, strictness: "strict" | "loose") {
if (strictness === "loose") {
value = +value
}
return value < options.max ?? 100;
// ~~~~~~~~~~~~~~~~~~~
// 错误:?? 的右操作数无法访问,因为左操作数永远不为空。
}
if (
isValid(primaryValue, "strict") || isValid(secondaryValue, "strict") ||
isValid(primaryValue, "loose" || isValid(secondaryValue, "loose"))
) {
// ~~~~~~~
// 错误:这种表达式总是真值。
}启用 ESLint 的 no-constant-binary-expression 规则也可以获得类似的结果,你可以在他们博客文章中看到他们捕获的一些结果; 但 TypeScript 执行的新检查与 ESLint 规则并不完全重叠,并且我们也相信将这些检查内置到 TypeScript 本身中有很大价值。
请注意,某些表达式即使总是真值或空值,仍然被允许。 具体来说,true、false、0 和 1 仍然被允许,尽管它们总是真值或假值,因为像下面这样的代码:
while (true) {
doStuff();
if (something()) {
break;
}
doOtherStuff();
}仍然是惯用且有用的,而像下面这样的代码:
if (true || inDebuggingOrDevelopmentEnvironment()) {
// ...
}在迭代/调试代码时很有用。
如果你对实现或它捕获的 bug 类型感到好奇,可以查看实现此功能的拉取请求。
迭代器辅助方法
JavaScript 有可迭代对象(我们可以通过调用 [Symbol.iterator]() 并获取迭代器来迭代的东西)和迭代器(具有 next() 方法的东西,我们可以调用它来尝试获取迭代中的下一个值)的概念。 总的来说,当你将它们放入 for/of 循环中,或 [...spread] 它们到新数组时,通常不需要考虑这些事情。 但 TypeScript 确实使用类型 Iterable 和 Iterator(甚至还有 IterableIterator,它同时扮演两者的角色!)对它们进行建模,这些类型描述了 for/of 等构造所需的最少成员集。
Iterable(和 IterableIterator)很好,因为它们可以在 JavaScript 的各种地方使用——但很多人发现自己想念 Array 上的方法,如 map、filter,以及出于某种原因的 reduce。 这就是为什么 ECMAScript 中提出了一个近期提案 将 Array 中的许多方法(甚至更多)添加到 JavaScript 中产生的大多数 IterableIterator 上。
例如,每个生成器现在都会产生一个也具有 map 方法和 take 方法的对象。
function* positiveIntegers() {
let i = 1;
while (true) {
yield i;
i++;
}
}
const evenNumbers = positiveIntegers().map(x => x * 2);
// 输出:
// 2
// 4
// 6
// 8
// 10
for (const value of evenNumbers.take(5)) {
console.log(value);
}对于 Map 和 Set 上的 keys()、values() 和 entries() 等方法也是如此。
function invertKeysAndValues<K, V>(map: Map<K, V>): Map<V, K> {
return new Map(
map.entries().map(([k, v]) => [v, k])
);
}你还可以扩展新的 Iterator 对象:
/**
* 提供无限个 `0` 的流。
*/
class Zeroes extends Iterator<number> {
next() {
return { value: 0, done: false } as const;
}
}
const zeroes = new Zeroes();
// 转换为无限个 `1` 的流。
const ones = zeroes.map(x => x + 1);你可以使用 Iterator.from 将任何现有的 Iterable 或 Iterator 适配到这种新类型中:
Iterator.from(...).filter(someFunction);现在,我们必须谈谈命名。
之前我们提到 TypeScript 有 Iterable 和 Iterator 的类型; 然而,正如我们提到的,这些类型有点像“协议”,用于确保某些操作有效。 这意味着并非每个在 TypeScript 中声明为 Iterable 或 Iterator 的值都会拥有我们上面提到的那些方法。
但是仍然有一个名为 Iterator 的新的运行时值。 你可以将 Iterator 以及 Iterator.prototype 作为 JavaScript 中的实际值来引用。 这有点尴尬,因为 TypeScript 已经定义了自己的名为 Iterator 的东西,纯粹用于类型检查。 因此,由于这种不幸的名称冲突,TypeScript 需要引入一个单独的类型来描述这些原生/内置的可迭代迭代器。
TypeScript 5.6 引入了一个名为 IteratorObject 的新类型。 其定义如下:
interface IteratorObject<T, TReturn = unknown, TNext = unknown> extends Iterator<T, TReturn, TNext> {
[Symbol.iterator](): IteratorObject<T, TReturn, TNext>;
}许多内置集合和方法都会产生 IteratorObject 的子类型(如 ArrayIterator、SetIterator、MapIterator 等),并且 lib.d.ts 中的核心 JavaScript 和 DOM 类型以及 @types/node 都已更新为使用这种新类型。
类似地,还有一个用于对称的 AsyncIteratorObject 类型。 AsyncIterator 在 JavaScript 中尚不存在作为运行时值,为 AsyncIterable 提供相同的方法,但这是一个活跃的提案,这个新类型为其做好准备。
我们要感谢 Kevin Gibbons 为这些类型的更改做出了贡献,他也是该提案的合著者之一。
严格的内置迭代器检查(以及 --strictBuiltinIteratorReturn)
当你在 Iterator<T, TReturn> 上调用 next() 方法时,它会返回一个带有 value 和 done 属性的对象。 这通过类型 IteratorResult 进行建模。
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 一个最终值——但两者之间的类型可以不相关。
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 方面遇到了一些困难。 同时,在 TReturn 为 any(默认值!)的情况下,IteratorResult 长期存在不安全的问题。 例如,假设我们有一个 IteratorResult<string, any>。 如果我们最终访问这个类型的 value,我们将得到 string | any,也就是 any。
function* uppercase(iter: Iterator<string, any>) {
while (true) {
const { value, done } = iter.next();
yield value.toUppercase(); // 哎呀!忘了先检查 `done` 并且拼错了 `toUpperCase`
if (done) {
return;
}
}
}在不引入许多破坏的情况下,今天很难在每个 Iterator 上修复这个问题,但我们至少可以修复大多数创建的 IteratorObject。
TypeScript 5.6 引入了一个名为 BuiltinIteratorReturn 的新固有类型和一个名为 --strictBuiltinIteratorReturn 的新 --strict 模式标志。 每当在 lib.d.ts 等地方使用 IteratorObject 时,它们总是用 BuiltinIteratorReturn 类型作为 TReturn 编写(尽管你会更常看到更具体的 MapIterator、ArrayIterator、SetIterator)。
interface MapIterator<T> extends IteratorObject<T, BuiltinIteratorReturn, unknown> {
[Symbol.iterator](): MapIterator<T>;
}
// ...
interface Map<K, V> {
// ...
/**
* 返回 map 中每个条目的键值对的可迭代对象。
*/
entries(): MapIterator<[K, V]>;
/**
* 返回 map 中键的可迭代对象。
*/
keys(): MapIterator<K>;
/**
* 返回 map 中值的可迭代对象。
*/
values(): MapIterator<V>;
}默认情况下,BuiltinIteratorReturn 是 any,但当启用 --strictBuiltinIteratorReturn(可能通过 --strict)时,它是 undefined。 在此新模式下,如果我们使用 BuiltinIteratorReturn,之前的示例现在会正确报错:
function* uppercase(iter: Iterator<string, BuiltinIteratorReturn>) {
while (true) {
const { value, done } = iter.next();
yield value.toUppercase();
// ~~~~~ ~~~~~~~~~~~
// 错误!┃ ┃
// ┃ ┗━ 类型 'string' 上不存在属性 'toUppercase'。你是想用 'toUpperCase' 吗?
// ┃
// ┗━ 'value' 可能为 'undefined'。
if (done) {
return;
}
}
}在整个 lib.d.ts 中,你通常会看到 BuiltinIteratorReturn 与 IteratorObject 配对使用。 总的来说,我们建议尽可能在自己的代码中更明确地指定 TReturn。
有关更多信息,你可以在此处阅读有关该功能的信息。
支持任意模块标识符
JavaScript 允许模块使用字符串字面量导出带有无效标识符名称的绑定:
const banana = "🍌";
export { banana as "🍌" };同样,它允许模块使用这些任意名称获取导入并将它们绑定到有效标识符:
import { "🍌" as banana } from "./foo"
/**
* 嗯嗯嗯嗯
*/
function eat(food: string) {
console.log("Eating", food);
};
eat(banana);这看起来像是一个有趣的把戏(如果你和我们一样在派对上玩得开心的话),但它对于与其他语言(通常通过 JavaScript/WebAssembly 边界)的互操作性有其用途,因为其他语言对于什么是有效标识符可能有不同的规则。 它对于生成代码的工具也很有用,比如 esbuild 的 inject 功能。
TypeScript 5.6 现在允许你在代码中使用这些任意模块标识符! 我们要感谢 Evan Wallace 为 TypeScript 贡献了这一更改!
--noUncheckedSideEffectImports 选项
在 JavaScript 中,可以 import 一个模块而不实际从中导入任何值。
import "some-module";这些导入通常被称为副作用导入,因为它们能提供的唯一有用行为是通过执行某些副作用(如注册全局变量,或向原型添加 polyfill)。
在 TypeScript 中,此语法有一个相当奇怪的怪癖:如果 import 可以解析到有效的源文件,那么 TypeScript 将加载并检查该文件。 另一方面,如果找不到源文件,TypeScript 会静默忽略该 import!
这是令人惊讶的行为,但它部分源于对 JavaScript 生态系统中的模式进行建模。 例如,此语法也已与打包器中的特殊加载器一起使用,以加载 CSS 或其他资源。 你的打包器可能以某种方式配置,使得你可以通过编写如下代码来包含特定的 .css 文件:
import "./button-component.css";
export function Button() {
// ...
}尽管如此,这掩盖了副作用导入中潜在的拼写错误。 这就是为什么 TypeScript 5.6 引入了一个名为 --noUncheckedSideEffectImports 的新编译器选项,以捕获这些情况。 当启用 --noUncheckedSideEffectImports 时,如果 TypeScript 找不到副作用导入的源文件,现在将报错。
import "oops-this-module-does-not-exist";
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// 错误:找不到模块 'oops-this-module-does-not-exist' 或其对应的类型声明。启用此选项后,一些正常工作的代码现在可能会收到错误,就像上面的 CSS 示例一样。 为了解决这个问题,想要仅为资源编写副作用 import 的用户可能更适合编写带有通配符说明符的所谓环境模块声明。 它应该放在一个全局文件中,看起来像这样:
// ./src/globals.d.ts
// 将所有 CSS 文件识别为模块导入。
declare module "*.css" {}事实上,你的项目中可能已经有这样一个文件了! 例如,运行 vite init 可能会创建一个类似的 vite-env.d.ts。
虽然此选项目前默认关闭,但我们鼓励用户尝试一下!
有关更多信息,请在此处查看实现。
--noCheck 选项
TypeScript 5.6 引入了一个新的编译器选项 --noCheck,它允许你跳过所有输入文件的类型检查。 当执行输出文件所需的任何语义分析时,这可以避免不必要的类型检查。
一种使用场景是将 JavaScript 文件生成与类型检查分离,以便两者可以作为独立的阶段运行。 例如,你可以在迭代时运行 tsc --noCheck,然后运行 tsc --noEmit 进行彻底的类型检查。 你甚至可以在 --watch 模式下并行运行这两个任务,不过请注意,如果真在同一时间运行它们,你可能需要指定一个单独的 --tsBuildInfoFile 路径。
--noCheck 对于以类似方式生成声明文件也很有用。 在符合 --isolatedDeclarations 的项目中指定 --noCheck 时,TypeScript 可以快速生成声明文件,而无需类型检查过程。 生成的声明文件将完全依赖于快速的语法转换。
请注意,在指定了 --noCheck 但项目未使用 --isolatedDeclarations 的情况下,TypeScript 可能仍然会执行生成 .d.ts 文件所需的类型检查。 从这个意义上说,--noCheck 有点名不副实;然而,该过程将比完整的类型检查更懒惰,仅计算未注解声明的类型。 这应该比完整的类型检查快得多。
noCheck 也可通过 TypeScript API 作为标准选项使用。 在内部,transpileModule 和 transpileDeclaration 已经使用 noCheck 来加速(至少在 TypeScript 5.5 中是这样)。 现在,任何构建工具都应该能够利用此标志,采用各种自定义策略来协调和加速构建。
有关更多信息,请参阅 TypeScript 5.5 中为在内部支持 noCheck 所做的工作,以及使其在命令行上可用的相关工作。
允许 --build 带有中间错误
TypeScript 的项目引用概念允许你将代码库组织成多个项目,并在它们之间创建依赖关系。 在 --build 模式下运行 TypeScript 编译器(或简称 tsc -b)是在项目间实际执行该构建并确定需要编译哪些项目和文件的内置方式。
以前,使用 --build 模式会假设 --noEmitOnError,如果遇到任何错误,会立即停止构建。 这意味着如果任何“上游”依赖项有构建错误,则永远无法检查和构建“下游”项目。 理论上,这是一种非常合理的方法——如果一个项目有错误,它不一定处于其依赖项所需的一致状态。
实际上,这种严格性使升级等事情变得很痛苦。 例如,如果 projectB 依赖于 projectA,那么更熟悉 projectB 的人无法主动升级其代码,直到他们的依赖项被升级。 他们被首先升级 projectA 的工作所阻塞。
从 TypeScript 5.6 开始,即使依赖项中存在中间错误,--build 模式也将继续构建项目。 面对中间错误,它们将被一致地报告,并且输出文件将尽力生成; 然而,构建将继续在指定的项目上完成。
如果你希望在第一个有错误的项目上停止构建,可以使用一个名为 --stopOnBuildErrors 的新标志。 这在 CI 环境中或在对其他项目有严重依赖的项目上进行迭代时可能很有用。
请注意,为了实现这一点,TypeScript 现在总是为 --build 调用中的任何项目输出一个 .tsbuildinfo 文件(即使没有指定 --incremental/--composite)。 这是为了跟踪 --build 是如何被调用的以及将来需要执行什么工作。
你可以在此处阅读有关此更改的更多信息。
编辑器中的区域优先诊断
当 TypeScript 的语言服务被请求一个文件的诊断(如错误、建议和弃用)时,它通常需要检查整个文件。 大多数情况下这没问题,但在非常大的文件中可能会造成延迟。 这可能会令人沮丧,因为修复一个拼写错误本应感觉是快速操作,但在足够大的文件中可能需要几秒钟。
为了解决这个问题,TypeScript 5.6 引入了一项名为区域优先诊断或区域优先检查的新功能。 编辑器现在不仅可以请求一组文件的诊断,还可以提供给定文件的相关区域——意图是这通常是用户当前可见的文件区域。 然后,TypeScript 语言服务器可以选择提供两组诊断:一组用于该区域,另一组用于整个文件。 这使得在大型文件中编辑感觉更响应迅速,这样你就不用等那么久才看到那些红色波浪线消失。
对于一些具体数字,在我们对 TypeScript 自身的 checker.ts 的测试中,完整的语义诊断响应耗时 3330 毫秒。 相比之下,首次基于区域的诊断响应耗时 143 毫秒! 而剩下的整个文件响应耗时约 3200 毫秒,这对于快速编辑来说可以产生巨大的差异。
此功能还包括大量工作,以使诊断在整个体验中报告更一致。 由于我们的类型检查器利用缓存来避免工作,相同类型之间的后续检查通常会有不同(通常更短)的错误消息。 从技术上讲,延迟的无序检查可能导致诊断在编辑器的两个位置之间报告不同——甚至在此功能之前也是如此——但我们不想加剧这个问题。 通过最近的工作,我们已经解决了许多这些错误不一致的问题。
目前,此功能在 Visual Studio Code 中可用于 TypeScript 5.6 及更高版本。
有关更详细的信息,请在此处查看实现和说明。
细粒度的提交字符
TypeScript 的语言服务现在为每个补全项提供自己的提交字符。 提交字符是特定的字符,当键入这些字符时,将自动提交当前建议的补全项。
这意味着,随着时间的推移,当你键入某些字符时,你的编辑器现在会更频繁地提交当前建议的补全项。 例如,看下面的代码:
declare let food: {
eat(): any;
}
let f = (foo/**/如果我们的光标在 /**/ 处,不清楚我们正在编写的代码将是 let f = (food.eat()) 还是 let f = (foo, bar) => foo + bar。 你可以想象,编辑器可能会根据我们接下来键入的字符以不同方式自动补全。 例如,如果我们键入点号字符(.),我们可能希望编辑器用变量 food 补全; 但如果我们键入逗号字符(,),我们可能正在编写箭头函数中的参数。
不幸的是,以前 TypeScript 只是向编辑器发出信号,表明当前文本可能定义一个新的参数名称,因此没有提交字符是安全的。 因此,即使“很明显”编辑器应该用单词 food 自动补全,键入 . 也不会做任何事情。
TypeScript 现在明确列出哪些字符对于每个补全项是安全的。 虽然这不会立即改变你的日常体验,但支持这些提交字符的编辑器应该会随着时间的推移看到行为上的改进。 要立即看到这些改进,你现在可以将 TypeScript nightly 扩展与 Visual Studio Code Insiders 一起使用。 在上面的代码中键入 . 会正确地用 food 自动补全。
有关更多信息,请参阅添加提交字符的拉取请求以及我们根据上下文调整提交字符的更改。
自动导入的排除模式
TypeScript 的语言服务现在允许你指定一个正则表达式模式列表,这些模式将过滤掉来自某些说明符的自动导入建议。 例如,如果你想排除来自包(如 lodash)的所有“深层”导入,你可以在 Visual Studio Code 中配置以下偏好:
{
"typescript.preferences.autoImportSpecifierExcludeRegexes": [
"^lodash/.*$"
]
}或者反过来,你可能想禁止从包的入口点导入:
{
"typescript.preferences.autoImportSpecifierExcludeRegexes": [
"^lodash$"
]
}甚至可以使用以下设置避免 node: 导入:
{
"typescript.preferences.autoImportSpecifierExcludeRegexes": [
"^node:"
]
}请注意,如果你想指定某些标志(如 i 或 u),你需要将正则表达式用斜杠括起来。 当提供周围的斜杠时,你需要转义其他内部的斜杠。
{
"typescript.preferences.autoImportSpecifierExcludeRegexes": [
"^./lib/internal", // 不需要转义
"/^.\\/lib\\/internal/", // 需要转义 - 注意前导和尾随的斜杠
"/^.\\/lib\\/internal/i" // 需要转义 - 我们需要斜杠来提供 'i' 正则标志
]
}在 Visual Studio Code 中,可以通过 javascript.preferences.autoImportSpecifierExcludeRegexes 为 JavaScript 应用相同的设置。
有关更多信息,请在此处查看实现。
值得注意的行为变化
本节重点介绍一组在升级时应该注意和理解的重要更改。 有时它会突出显示弃用、删除和新限制。 它还可能包含功能上改进的错误修复,但也可能通过引入新错误来影响现有构建。
lib.d.ts
为 DOM 生成的类型可能会对代码库的类型检查产生影响。 有关更多信息,请查看与此版本 TypeScript 的 DOM 和 lib.d.ts 更新相关的链接问题。
始终写入 .tsbuildinfo
为了启用 --build 即使在依赖项中存在中间错误时也能继续构建项目,并支持命令行上的 --noCheck,TypeScript 现在总是为 --build 调用中的任何项目输出一个 .tsbuildinfo 文件。 无论是否实际启用了 --incremental,都会发生这种情况。 在此处查看更多信息。
尊重 node_modules 中的文件扩展名和 package.json
在 Node.js 于 v12 中实现对 ECMAScript 模块的支持之前,TypeScript 一直没有一个好方法来知道它在 node_modules 中找到的 .d.ts 文件表示的是以 CommonJS 还是 ECMAScript 模块编写的 JavaScript 文件。 当 npm 的绝大多数是仅 CommonJS 时,这并没有造成很多问题——如有疑问,TypeScript 可以假设一切行为都像 CommonJS。 不幸的是,如果这个假设是错误的,它可能会允许不安全的导入:
// node_modules/dep/index.d.ts
export declare function doSomething(): void;
// index.ts
// 如果 "dep" 是 CommonJS 模块,则可以,但如果它是 ECMAScript 模块,则失败 - 即使在打包器中也是如此!
import dep from "dep";
dep.doSomething();在实践中,这并不经常出现。 但在 Node.js 开始支持 ECMAScript 模块以来的几年里,npm 上 ESM 的份额已经增长。 幸运的是,Node.js 还引入了一种可以帮助 TypeScript 确定文件是 ECMAScript 模块还是 CommonJS 模块的机制:.mjs 和 .cjs 文件扩展名以及 package.json "type" 字段。 TypeScript 4.7 增加了对这些指示器的支持,以及编写 .mts 和 .cts 文件; 然而,TypeScript 仅在 --module node16 和 --module nodenext 下读取这些指示器,因此对于使用 --module esnext 和 --moduleResolution bundler 的人来说,上面的不安全导入仍然是一个问题。
为了解决这个问题,TypeScript 5.6 收集模块格式信息,并在所有 module 模式(除了 amd、umd 和 system)中使用它来解决像上面例子中的歧义。 在任何找到格式特定文件扩展名(.mts 和 .cts)的地方都会尊重它们,并且无论 module 设置如何,都会查阅 node_modules 依赖项中的 package.json "type" 字段。 以前,技术上可能将 CommonJS 输出到 .mjs 文件中,反之亦然:
// 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。
请注意,这些行为的大部分已在 TypeScript 5.5 的预发布版本中提供(实现细节在此处),但在 5.6 中,此行为仅扩展到 node_modules 中的文件。
更多细节可在此处查看更改。
对计算属性的正确 override 检查
以前,标记为 override 的计算属性未正确检查基类成员是否存在。 类似地,如果使用 noImplicitOverride,如果你忘记向计算属性添加 override 修饰符,你不会得到错误。
TypeScript 5.6 现在在这两种情况下都能正确检查计算属性。
const foo = Symbol("foo");
const bar = Symbol("bar");
class Base {
[bar]() {}
}
class Derived extends Base {
override [foo]() {}
// ~~~~~
// 错误:此成员不能有 'override' 修饰符,因为它未在基类 'Base' 中声明。
[bar]() {}
// ~~~~~
// 在 noImplicitOverride 下错误:此成员必须具有 'override' 修饰符,因为它覆盖了基类 'Base' 中的成员。
}此修复由 Oleksandr Tarasiuk 在此拉取请求中贡献。