允许在构造函数中 super() 之前编写代码
在 JavaScript 类中,必须在引用 this 之前调用 super()。TypeScript 也强制执行这一点,尽管它在确保这一点的方式上有点过于严格。在 TypeScript 中,如果一个类的构造函数包含任何属性初始化器,那么在构造函数的开头包含任何代码以前都是错误的。
tsclass Base {// ...}class Derived extends Base {someProperty = true;constructor() {// error!// have to call 'super()' first because it needs to initialize 'someProperty'.doSomeStuff();super();}}
这使得检查 super() 在引用 this 之前被调用变得很简单,但它最终拒绝了许多有效的代码。TypeScript 4.6 现在对该检查更加宽松,允许在 super() 之前运行其他代码,同时仍然确保 super() 出现在任何引用 this 之前的顶层位置。
我们要感谢 Joshua Goldberg 耐心与我们合作完成此项更改!
解构判别式联合类型的控制流分析
TypeScript 能够根据所谓的判别属性来收窄类型。例如,在下面的代码片段中,TypeScript 能够在我们每次检查 kind 的值时收窄 action 的类型。
tstype Action =| { kind: "NumberContents"; payload: number }| { kind: "StringContents"; payload: string };function processAction(action: Action) {if (action.kind === "NumberContents") {// `action.payload` is a number here.let num = action.payload * 2;// ...} else if (action.kind === "StringContents") {// `action.payload` is a string here.const str = action.payload.trim();// ...}}
这使我们能够处理可以保存不同数据的对象,但一个公共字段告诉我们这些对象拥有哪种数据。
这在 TypeScript 中非常常见;然而,根据您的偏好,您可能希望在上面的示例中解构 kind 和 payload。也许类似于以下内容
tstype Action =| { kind: "NumberContents"; payload: number }| { kind: "StringContents"; payload: string };function processAction(action: Action) {const { kind, payload } = action;if (kind === "NumberContents") {let num = payload * 2;// ...} else if (kind === "StringContents") {const str = payload.trim();// ...}}
以前 TypeScript 会在这些代码上报错——一旦 kind 和 payload 从同一个对象中提取到变量中,它们就被视为完全独立的。
在 TypeScript 4.6 中,这可以直接工作了!
当将单个属性解构到 const 声明中,或者将参数解构为从未被重新赋值的变量时,TypeScript 将检查被解构的类型是否为判别式联合类型。如果是,TypeScript 现在可以根据其他变量的检查来收窄变量的类型。因此在我们的示例中,对 kind 的检查会收窄 payload 的类型。
更多信息,请参阅实现此分析的拉取请求。
改进的递归深度检查
由于 TypeScript 是建立在同时提供泛型的结构化类型系统之上的,因此它面临一些有趣的挑战。
在结构化类型系统中,对象类型的兼容性取决于它们拥有的成员。
tsinterface Source {prop: string;}interface Target {prop: number;}function check(source: Source, target: Target) {target = source;// error!// Type 'Source' is not assignable to type 'Target'.// Types of property 'prop' are incompatible.// Type 'string' is not assignable to type 'number'.}
请注意,Source 是否与 Target 兼容与它们的属性是否可赋值有关。在这种情况下,也就是 prop。
当您向其中引入泛型时,会有一些更难回答的问题。例如,在以下情况下,Source<string> 可赋值给 Target<number> 吗?
tsinterface Source<T> {prop: Source<Source<T>>;}interface Target<T> {prop: Target<Target<T>>;}function check(source: Source<string>, target: Target<number>) {target = source;}
为了回答这个问题,TypeScript 需要检查 prop 的类型是否兼容。这导致了另一个问题:Source<Source<string>> 可赋值给 Target<Target<number>> 吗?为了回答这个问题,TypeScript 会检查 prop 对于那些类型是否兼容,并最终检查 Source<Source<Source<string>>> 是否可赋值给 Target<Target<Target<number>>>。继续下去,您可能会注意到挖掘得越深,类型扩展得就越无限。
TypeScript 在这里有一些启发式方法——如果一个类型在遇到一定的深度检查后看起来是无限扩展的,那么它会认为这些类型可能是兼容的。这通常就足够了,但令人尴尬的是,有一些假阴性是这种方法无法捕捉到的。
tsinterface Foo<T> {prop: T;}declare let x: Foo<Foo<Foo<Foo<Foo<Foo<string>>>>>>;declare let y: Foo<Foo<Foo<Foo<Foo<string>>>>>;x = y;
人类读者可以看到上面的例子中 x 和 y 应该是互不兼容的。虽然类型嵌套很深,但这只是它们声明方式的结果。这种启发式方法旨在捕捉那些通过探索类型而生成的深层嵌套类型的情况,而不是开发人员自己编写的那种。
TypeScript 4.6 现在能够区分这些情况,并正确地对最后一个示例报错。此外,由于语言不再担心显式编写的类型带来的误报,TypeScript 可以更早地断定一个类型是无限扩展的,从而在检查类型兼容性时节省大量工作。结果是,DefinitelyTyped 上的 redux-immutable、react-lazylog 和 yup 等库的检查时间减少了 50%。
您可能已经拥有此更改,因为它被 cherry-picked 到了 TypeScript 4.5.3 中,但它是 TypeScript 4.6 的一个显著特性,您可以在此处阅读更多相关信息。
索引访问类型推断改进
TypeScript 现在可以正确推断出直接索引到映射对象类型的索引访问类型。
tsinterface TypeMap {number: number;string: string;boolean: boolean;}type UnionRecord<P extends keyof TypeMap> = {[K in P]: {kind: K;v: TypeMap[K];f: (p: TypeMap[K]) => void;};}[P];function processRecord<K extends keyof TypeMap>(record: UnionRecord<K>) {record.f(record.v);}// This call used to have issues - now works!processRecord({kind: "string",v: "hello!",// 'val' used to implicitly have the type 'string | number | boolean',// but now is correctly inferred to just 'string'.f: (val) => {console.log(val.toUpperCase());},});
这种模式早已得到支持,并使 TypeScript 能够理解对 record.f(record.v) 的调用是有效的,但之前对 processRecord 的调用会导致 val 的推断结果很差。
TypeScript 4.6 改进了这一点,因此在调用 processRecord 时不再需要类型断言。
更多信息,您可以阅读拉取请求。
相关参数的控制流分析
可以声明一个具有剩余参数(rest parameter)的签名,其类型是元组的判别式联合类型。
tsfunction func(...args: ["str", string] | ["num", number]) {// ...}
这表示 func 的参数完全取决于第一个参数。当第一个参数是字符串 "str" 时,第二个参数必须是 string。当第一个参数是字符串 "num" 时,第二个参数必须是 number。
在 TypeScript 从这样的签名中推断函数类型的情况下,TypeScript 现在可以收窄相互依赖的参数。
tstype Func = (...args: ["a", number] | ["b", string]) => void;const f1: Func = (kind, payload) => {if (kind === "a") {payload.toFixed(); // 'payload' narrowed to 'number'}if (kind === "b") {payload.toUpperCase(); // 'payload' narrowed to 'string'}};f1("a", 42);f1("b", "hello");
更多信息,请参阅 GitHub 上的更改。
--target es2022
TypeScript 的 --target 选项现在支持 es2022。这意味着类字段等特性现在有了稳定的输出目标,可以被保留下来。这也意味着可以使用新的内置功能,例如 Array 的 at() 方法、Object.hasOwn 或 new Error 上的 cause 选项,既可以通过这个新的 --target 设置使用,也可以通过 --lib es2022 使用。
此功能由 Kagami Sascha Rosylight (saschanaz) 通过多个 PR 实现,我们非常感谢这一贡献!
在 react-jsx 中移除不必要的参数
此前,在 --jsx react-jsx 下编译如下代码时
tsxexport const el = <div>foo</div>;
TypeScript 会生成以下 JavaScript 代码
jsximport { jsx as _jsx } from "react/jsx-runtime";export const el = _jsx("div", { children: "foo" }, void 0);
最后一个 void 0 参数在此编译模式下是不必要的,删除它可以减小包体积。
diff- export const el = _jsx("div", { children: "foo" }, void 0);+ export const el = _jsx("div", { children: "foo" });
感谢来自 Alexander Tarasyuk 的拉取请求,TypeScript 4.6 现在删除了 void 0 参数。
JSDoc 名称建议
在 JSDoc 中,您可以使用 @param 标签记录参数。
js/*** @param x The first operand* @param y The second operand*/function add(x, y) {return x + y;}
但是当这些注释过期时会发生什么呢?如果我们把 x 和 y 重命名为 a 和 b 会怎样?
js/*** @param x {number} The first operand* @param y {number} The second operand*/function add(a, b) {return a + b;}
此前,TypeScript 仅在对 JavaScript 文件执行类型检查时(即使用 checkJs 选项,或在文件顶部添加 // @ts-check 注释时)才会提醒您这一点。
现在,您可以在编辑器中获得 TypeScript 文件的类似信息!当参数名称在函数与其 JSDoc 注释之间不匹配时,TypeScript 现在会提供建议。

此更改由 Alexander Tarasyuk 提供!
JavaScript 中的更多语法和绑定错误
TypeScript 扩展了其在 JavaScript 文件中的语法和绑定错误集合。如果您在 Visual Studio 或 Visual Studio Code 等编辑器中打开 JavaScript 文件,或者通过 TypeScript 编译器运行 JavaScript 代码(即使您没有开启 checkJs 或在文件顶部添加 // @ts-check 注释),您也会看到这些新错误。
举个例子,如果您在 JavaScript 文件的同一作用域中有两个 const 的声明,TypeScript 现在会对这些声明发出错误。
tsconst foo = 1234;// ~~~// error: Cannot redeclare block-scoped variable 'foo'.// ...const foo = 5678;// ~~~// error: Cannot redeclare block-scoped variable 'foo'.
再举一个例子,如果您错误地使用了修饰符,TypeScript 会告知您。
tsfunction container() {export function foo() {// ~~~~~~// error: Modifiers cannot appear here.}}
可以通过在文件顶部添加 // @ts-nocheck 来禁用这些错误,但我们很有兴趣听取一些关于它如何影响您的 JavaScript 工作流程的早期反馈。您可以通过安装 TypeScript and JavaScript Nightly 扩展轻松在 Visual Studio Code 中尝试它,并阅读第一个和第二个拉取请求以了解更多信息。
TypeScript 跟踪分析器
偶尔,团队可能会遇到创建和与其他类型进行比较时计算成本高昂的类型。TypeScript 有一个 --generateTrace 标志来帮助识别其中一些昂贵的类型,或者有时帮助诊断 TypeScript 编译器中的问题。虽然 --generateTrace 生成的信息可能很有用(特别是 TypeScript 4.6 中添加的一些信息),但它在现有的跟踪可视化工具中通常难以阅读。
我们最近发布了一个名为 @typescript/analyze-trace 的工具,可以更直观地查看此信息。虽然我们不指望每个人都需要 analyze-trace,但我们认为对于任何遇到 TypeScript 构建性能问题的团队来说,它都能派上用场。
更多信息,请参阅 analyze-trace 工具的仓库。
破坏性变更
对象剩余参数从泛型对象中剔除不可展开的成员
对象剩余表达式现在剔除泛型对象上看起来不可展开的成员。在下面的示例中……
tsclass Thing {someProperty = 42;someMethod() {// ...}}function foo<T extends Thing>(x: T) {let { someProperty, ...rest } = x;// Used to work, is now an error!// Property 'someMethod' does not exist on type 'Omit<T, "someProperty" | "someMethod">'.rest.someMethod();}
变量 rest 曾经具有 Omit<T, "someProperty"> 类型,因为 TypeScript 会严格分析哪些其他属性被解构了。这并没有模拟出 ...rest 在非泛型类型解构中是如何工作的,因为 someMethod 通常也会被剔除。在 TypeScript 4.6 中,rest 的类型是 Omit<T, "someProperty" | "someMethod">。
这种情况也会出现在从 this 解构时。当使用 ...rest 元素解构 this 时,不可展开和非公共成员现在会被剔除,这与在其他地方解构类实例是一致的。
tsclass Thing {someProperty = 42;someMethod() {// ...}someOtherMethod() {let { someProperty, ...rest } = this;// Used to work, is now an error!// Property 'someMethod' does not exist on type 'Omit<T, "someProperty" | "someMethod">'.rest.someMethod();}}
更多细节,请参阅此处的相应更改。
JavaScript 文件始终接收语法和绑定错误
此前,TypeScript 除了忽略在 JavaScript 文件中意外使用 TypeScript 语法的情况外,会忽略大多数 JavaScript 中的语法错误。TypeScript 现在会在您的文件中显示 JavaScript 语法和绑定错误,例如使用不正确的修饰符、重复声明等。这些通常在 Visual Studio Code 或 Visual Studio 中最为明显,但也可能在通过 TypeScript 编译器运行 JavaScript 代码时出现。
您可以通过在文件顶部插入 // @ts-nocheck 注释来明确关闭这些错误。