TypeScript 4.6
允许在 super() 之前编写构造函数代码
在 JavaScript 类中,必须在引用 this 之前调用 super()。 TypeScript 也强制执行这一点,但在如何确保这一点方面有点过于严格。 在 TypeScript 中,如果包含类有任何属性初始化器,则在构造函数开头包含任何代码以前是错误。
class Base {
// ...
}
class Derived extends Base {
someProperty = true;
constructor() {
// 错误!
// 必须首先调用 'super()',因为它需要初始化 'someProperty'。
doSomeStuff();
super();
}
}这使得检查 super() 在引用 this 之前被调用变得简单,但它最终拒绝了许多有效代码。 TypeScript 4.6 在该检查中更加宽松,允许在 super() 之前运行其他代码,同时仍然确保 super() 发生在任何对 this 的引用之前的顶层。
我们要感谢 Joshua Goldberg 耐心地与我们合作实现此更改!
解构可辨识联合的控制流分析
TypeScript 能够基于所谓的可辨识属性来收窄类型。 例如,在以下代码片段中,TypeScript 能够在每次检查 kind 的值时收窄 action 的类型。
type Action =
| { kind: "NumberContents"; payload: number }
| { kind: "StringContents"; payload: string };
function processAction(action: Action) {
if (action.kind === "NumberContents") {
// 这里 `action.payload` 是一个数字。
let num = action.payload * 2;
// ...
} else if (action.kind === "StringContents") {
// 这里 `action.payload` 是一个字符串。
const str = action.payload.trim();
// ...
}
}这让我们可以使用可以保存不同数据的对象,但一个公共字段告诉我们这些对象拥有哪些数据。
这在 TypeScript 中非常常见;然而,根据你的偏好,你可能希望在上面的示例中解构 kind 和 payload。 也许像下面这样:
type 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 建立在同时提供泛型的结构类型系统之上,因此它面临一些有趣的挑战。
在结构类型系统中,对象类型基于它们具有的成员而兼容。
interface Source {
prop: string;
}
interface Target {
prop: number;
}
function check(source: Source, target: Target) {
target = source;
// 错误!
// 类型 'Source' 不能赋值给类型 'Target'。
// 属性 'prop' 的类型不兼容。
// 类型 'string' 不能赋值给类型 'number'。
}注意 Source 是否与 Target 兼容取决于它们的属性是否可赋值。 在这种情况下,就是 prop。
当引入泛型时,有一些更难回答的问题。 例如,在以下情况下,Source<string> 是否可赋值给 Target<number>?
interface 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 在这里有一些启发式方法——如果一种类型在遇到一定深度检查后似乎无限扩展,那么它认为这些类型可能是兼容的。 这通常足够了,但令人尴尬的是,有一些假阴性是它无法捕获的。
interface 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 可以更早地得出结论,即类型无限扩展,并节省大量检查类型兼容性的工作。 因此,像 redux-immutable、react-lazylog 和 yup 这样的 DefinitelyTyped 库的检查时间减少了 50%。
你可能已经拥有此更改,因为它被 cherry-pick 到了 TypeScript 4.5.3 中,但它是 TypeScript 4.6 的一个显著特性,你可以在此处阅读更多信息。
索引访问推断改进
TypeScript 现在可以正确推断立即索引到映射对象类型的索引访问类型。
interface 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);
}
// 此调用以前有问题 - 现在有效!
processRecord({
kind: "string",
v: "hello!",
// 'val' 以前隐式具有类型 'string | number | boolean',
// 但现在被正确推断为 'string'。
f: (val) => {
console.log(val.toUpperCase());
},
});此模式已经得到支持,并允许 TypeScript 理解对 record.f(record.v) 的调用是有效的,但以前对 processRecord 的调用会为 val 提供较差的推断结果。
TypeScript 4.6 改进了这一点,因此在调用 processRecord 时不再需要类型断言。
有关更多信息,你可以阅读拉取请求。
依赖参数的控制流分析
可以使用一个剩余参数来声明一个签名,其类型是元组的可辨识联合。
function func(...args: ["str", string] | ["num", number]) {
// ...
}这意味着 func 的参数完全取决于第一个参数。 当第一个参数是字符串 "str" 时,它的第二个参数必须是 string。 当第一个参数是字符串 "num" 时,它的第二个参数必须是 number。
在 TypeScript 从这样的签名推断函数类型的情况下,TypeScript 现在可以收窄相互依赖的参数。
type Func = (...args: ["a", number] | ["b", string]) => void;
const f1: Func = (kind, payload) => {
if (kind === "a") {
payload.toFixed(); // 'payload' 收窄为 'number'
}
if (kind === "b") {
payload.toUpperCase(); // 'payload' 收窄为 '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 下编译如下代码时
export const el = <div>foo</div>;TypeScript 会生成以下 JavaScript 代码:
import { jsx as _jsx } from "react/jsx-runtime";
export const el = _jsx("div", { children: "foo" }, void 0);最后的 void 0 参数在此输出模式中是不必要的,删除它可以提高打包大小。
- 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 标签记录参数。
/**
* @param x 第一个操作数
* @param y 第二个操作数
*/
function add(x, y) {
return x + y;
}但是当这些注释过时时会发生什么? 如果我们把 x 和 y 重命名为 a 和 b 呢?
/**
* @param x {number} 第一个操作数
* @param y {number} 第二个操作数
*/
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 现在会对这些声明发出错误。
const foo = 1234;
// ~~~
// 错误:无法重新声明块作用域变量 'foo'。
// ...
const foo = 5678;
// ~~~
// 错误:无法重新声明块作用域变量 'foo'。再比如,TypeScript 会告诉你修饰符是否被错误使用。
function container() {
export function foo() {
// ~~~~~~
// 错误:修饰符不能出现在这里。
}
}通过在文件顶部添加 // @ts-nocheck 可以禁用这些错误,但我们有兴趣听到一些关于它如何适应你的 JavaScript 工作流程的早期反馈。 你可以通过安装 TypeScript 和 JavaScript 夜间扩展在 Visual Studio Code 中轻松试用,并阅读更多关于第一和第二个拉取请求的信息。
TypeScript 跟踪分析器
有时,团队可能会遇到计算成本高昂且难以与其他类型进行比较的类型。 TypeScript 有一个 --generateTrace 标志来帮助识别其中一些昂贵的类型,或有时帮助诊断 TypeScript 编译器中的问题。 虽然 --generateTrace 生成的信息可能很有用(尤其是 TypeScript 4.6 中添加了一些信息),但在现有的跟踪可视化工具中通常难以阅读。
我们最近发布了一个名为 @typescript/analyze-trace 的工具,以获得更易理解的此信息视图。 虽然我们不期望每个人都需 analyze-trace,但我们认为它对任何遇到 TypeScript 构建性能问题的团队都很有用。
有关更多信息,请参阅 analyze-trace 工具的存储库。
破坏性更改
对象剩余表达式从泛型对象中删除不可展开的成员
对象剩余表达式现在从泛型对象中删除看起来不可展开的成员。 在以下示例中...
class Thing {
someProperty = 42;
someMethod() {
// ...
}
}
function foo<T extends Thing>(x: T) {
let { someProperty, ...rest } = x;
// 以前有效,现在报错!
// 类型 'Omit<T, "someProperty" | "someMethod">' 上不存在属性 'someMethod'。
rest.someMethod();
}变量 rest 以前具有类型 Omit<T, "someProperty">,因为 TypeScript 会严格分析哪些其他属性被解构。 这不能模拟 ...rest 在非泛型类型的解构中如何工作,因为 someMethod 通常也会被丢弃。 在 TypeScript 4.6 中,rest 的类型是 Omit<T, "someProperty" | "someMethod">。
这也可能发生在从 this 解构时。 当使用 ...rest 元素解构 this 时,不可展开和非公共成员现在被丢弃,这与在其他地方解构类的实例一致。
class Thing {
someProperty = 42;
someMethod() {
// ...
}
someOtherMethod() {
let { someProperty, ...rest } = this;
// 以前有效,现在报错!
// 类型 'Omit<T, "someProperty" | "someMethod">' 上不存在属性 'someMethod'。
rest.someMethod();
}
}有关更多详细信息,请在此处查看相应的更改。
JavaScript 文件始终接收语法和绑定错误
以前,除了在 JavaScript 文件中意外使用 TypeScript 语法之外,TypeScript 会忽略大多数语法错误。 TypeScript 现在显示文件中的 JavaScript 语法和绑定错误,例如使用不正确的修饰符、重复声明等。 这些错误通常在 Visual Studio Code 或 Visual Studio 中最明显,但也可能通过 TypeScript 编译器运行 JavaScript 代码时发生。
你可以通过在文件顶部插入 // @ts-nocheck 注释来显式关闭这些错误。