TypeScript 4.6

允许在构造函数中的 super() 调用之前插入代码

在 JavaScript 的类中,在引用 this 之前必须先调用 super()。 在 TypeScript 中同样有这个限制,只不过在检查时过于严格。 在之前版本的 TypeScript 中,如果类中存在属性初始化器, 那么在构造函数里,在 super() 调用之前不允许出现任何其它代码。

  1. class Base {
  2. // ...
  3. }
  4. class Derived extends Base {
  5. someProperty = true;
  6. constructor() {
  7. // 错误!
  8. // 必须先调用 'super()' 因为需要初始化 'someProperty'。
  9. doSomeStuff();
  10. super();
  11. }
  12. }

这样做是因为程序实现起来容易,但这样做也会拒绝很多合法的代码。 TypeScript 4.6 放宽了限制,它允许在 super() 之前出现其它代码, 与此同时仍然会检查在引用 this 之前顶层的super() 已经被调用。

感谢 Joshua GoldbergPR

基于控制流来分析解构的可辨识联合类型

TypeScript 可以根据判别式属性来细化类型。 例如,在下面的代码中,TypeScript 能够在检查 kind 的类型后细化 action 的类型。

  1. type Action =
  2. | { kind: "NumberContents", payload: number }
  3. | { kind: "StringContents", payload: string };
  4. function processAction(action: Action) {
  5. if (action.kind === "NumberContents") {
  6. // `action.payload` is a number here.
  7. let num = action.payload * 2
  8. // ...
  9. }
  10. else if (action.kind === "StringContents") {
  11. // `action.payload` is a string here.
  12. const str = action.payload.trim();
  13. // ...
  14. }
  15. }

这样就可以使用持有不同数据的对象,但通过共同的字段来区分它们。

这在 TypeScript 是很常见的;然而,根据个人的喜好,你可能想对上例中的 kindpayload 进行解构。 就像下面这样:

  1. type Action =
  2. | { kind: "NumberContents", payload: number }
  3. | { kind: "StringContents", payload: string };
  4. function processAction(action: Action) {
  5. const { kind, payload } = action;
  6. if (kind === "NumberContents") {
  7. let num = payload * 2
  8. // ...
  9. }
  10. else if (kind === "StringContents") {
  11. const str = payload.trim();
  12. // ...
  13. }
  14. }

此前,TypeScript 会报错 - 当 kindpayload 是由同一个对象解构为变量时,它们会被独立对待。

在 TypeScript 4.6 中可以正常工作!

当解构独立的属性为 const 声明,或当解构参数到变量且没有重新赋值时,TypeScript 会检查被解构的类型是否为可辨识联合。 如果是的话,TypeScript 就能够根据类型检查来细化变量的类型。 因此上例中,通过检查 kind 的类型可以细化 payload 的类型。

更多详情请查看 PR

改进的递归深度检查

TypeScript 要面对一些有趣的挑战,因为它是构建在结构化类型系统之上,同时又支持了泛型。

在结构化类型系统中,对象类型的兼容性是由对象包含的成员决定的。

  1. interface Source {
  2. prop: string;
  3. }
  4. interface Target {
  5. prop: number;
  6. }
  7. function check(source: Source, target: Target) {
  8. target = source;
  9. // error!
  10. // Type 'Source' is not assignable to type 'Target'.
  11. // Types of property 'prop' are incompatible.
  12. // Type 'string' is not assignable to type 'number'.
  13. }

SourceTarget 的兼容性取决于它们的属性是否可以执行赋值操作。 此例中是指 prop 属性。

当引入了泛型后,有一些难题需要解决。 例如,下例中的 Source<string> 是否可以赋值给 Target<number>

  1. interface Source<T> {
  2. prop: Source<Source<T>>;
  3. }
  4. interface Target<T> {
  5. prop: Target<Target<T>>;
  6. }
  7. function check(source: Source<string>, target: Target<number>) {
  8. target = source;
  9. }

要想回答这个问题,TypeScript 需要检查 prop 的类型是否兼容。 这又要回答另一个问题:Source<Source<string>> 是否能够赋值给 Target<Target<number>>? 要想回答这个问题,TypeScript 需要检查 prop 的类型是否与那些类型兼容, 结果就是还要检查 Source<Source<Source<string>>> 是否能够赋值给 Target<Target<Target<number>>>? 继续发展下去,就会注意到类型会进行无限展开。

TypeScript 使用了启发式的算法 - 当一个类型达到特定的检查深度时,它表现出了将会进行无限展开, 那么就认为它可能是兼容的。 通常情况下这是没问题的,但是也可能出现漏报的情况。

  1. interface Foo<T> {
  2. prop: T;
  3. }
  4. declare let x: Foo<Foo<Foo<Foo<Foo<Foo<string>>>>>>;
  5. declare let y: Foo<Foo<Foo<Foo<Foo<string>>>>>;
  6. x = y;

通过人眼观察我们知道上例中的 xy 是不兼容的。 虽然类型的嵌套层次很深,但人家就是这样声明的。 启发式算法要处理的是在探测类型过程中生成的深层次嵌套类型,而非程序员明确手写出的类型。

TypeScript 4.6 现在能够区分出这类情况,并且对上例进行正确的错误提示。 此外,由于不再担心会对明确书写的类型进行误报, TypeScript 能够更容易地判断类型的无限展开, 并且降低了类型兼容性检查的成本。 因此,像 DefinitelyTyped 上的 redux-immutablereact-lazylogyup 代码库,对它们的类型检查时间降低了 50%。

你可能已经体验过这个改动了,因为它被挑选合并到了 TypeScript 4.5.3 中, 但它仍然是 TypeScript 4.6 中值得关注的一个特性。 更多详情请阅读 PR

索引访问类型推断改进

TypeScript 现在能够正确地推断通过索引访问到另一个映射对象类型的类型。

  1. interface TypeMap {
  2. "number": number;
  3. "string": string;
  4. "boolean": boolean;
  5. }
  6. type UnionRecord<P extends keyof TypeMap> = { [K in P]:
  7. {
  8. kind: K;
  9. v: TypeMap[K];
  10. f: (p: TypeMap[K]) => void;
  11. }
  12. }[P];
  13. function processRecord<K extends keyof TypeMap>(record: UnionRecord<K>) {
  14. record.f(record.v);
  15. }
  16. // 这个调用之前是有问题的,但现在没有问题
  17. processRecord({
  18. kind: "string",
  19. v: "hello!",
  20. // 'val' 之前会隐式地获得类型 'string | number | boolean',
  21. // 但现在会正确地推断为类型 'string'。
  22. f: val => {
  23. console.log(val.toUpperCase());
  24. }
  25. })

该模式已经被支持了并允许 TypeScript 判断 record.f(record.v) 调用是合理的, 但是在以前,processRecord 调用中对 val 的类型推断并不好。

TypeScript 4.6 改进了这个情况,因此在启用 processRecord 时不再需要使用类型断言。

更多详情请阅读 PR

对因变参数的控制流分析

函数签名可以声明为剩余参数且其类型可以为可辨识联合元组类型。

  1. function func(...args: ["str", string] | ["num", number]) {
  2. // ...
  3. }

这意味着 func 的实际参数完全依赖于第一个实际参数。 若第一个参数为字符串 "str" 时,则第二个参数为 string 类型。 若第一个参数为字符串 "num" 时,则第二个参数为 number 类型。

像这样 TypeScript 是由签名来推断函数类型时,TypeScript 能够根据依赖的参数来细化类型。

  1. type Func = (...args: ["a", number] | ["b", string]) => void;
  2. const f1: Func = (kind, payload) => {
  3. if (kind === "a") {
  4. payload.toFixed(); // 'payload' narrowed to 'number'
  5. }
  6. if (kind === "b") {
  7. payload.toUpperCase(); // 'payload' narrowed to 'string'
  8. }
  9. };
  10. f1("a", 42);
  11. f1("b", "hello");

更多详情请阅读 PR

—target es2022

TypeScript 的 --target 编译选项现在支持使用 es2022。 这意味着像类字段这样的特性能够稳定地在输出结果中保留。 这也意味着像 Arrays 的上 at() 和 Object.hasOwn 方法 或者 new Error 时的 cause 选项 可以通过设置新的 --target 或者 --lib es2022 来使用。

感谢 Kagami Sascha Rosylight (saschanaz)实现

删除 react-jsx 中不必要的参数

在以前,当使用 --jsx react-jsx 来编译如下的代码时

  1. export const el = <div>foo</div>;

TypeScript 会生成如下的 JavaScript 代码:

  1. import { jsx as _jsx } from "react/jsx-runtime";
  2. export const el = _jsx("div", { children: "foo" }, void 0);

末尾的 void 0 参数是没用的,删掉它会减小打包的体积。

感谢 https://github.com/a-tarasyukPR,TypeScript 4.6 会删除 void 0 参数。

JSDoc 命名建议

在 JSDoc 里,你可以用 @param 标签来文档化参数。

  1. /**
  2. * @param x The first operand
  3. * @param y The second operand
  4. */
  5. function add(x, y) {
  6. return x + y;
  7. }

但是,如果这些注释已经过时了会发生什么?就比如,我们将 xy 重命名为 ab

  1. /**
  2. * @param x {number} The first operand
  3. * @param y {number} The second operand
  4. */
  5. function add(a, b) {
  6. return a + b;
  7. }

在之前 TypeScript 仅会在对 JavaScript 文件执行类型检查时报告这个问题 - 通过 使用 checkJs 选项,或者在文件顶端添加 // @ts-check 注释。

现在,你能够在编译器中的 TypeScript 文件上看到类似的提示! TypeScript 现在会给出建议,如果函数签名中的参数名与 JSDoc 中的参数名不一致。

example

改动是由 Alexander Tarasyuk 提供的!

JavaScript 中更多的语法和绑定错误提示

TypeScript 将更多的语法和绑定错误检查应用到了 JavaScript 文件上。 如果你在 Visual Studio 或 Visual Studio Code 这样的编辑器中打开 JavaScript 文件时就会看到这些新的错误提示, 或者当你使用 TypeScript 编译器来处理 JavaScript 文件时 - 即便你没有打开 checkJs 或者添加 // @ts-check 注释。

做为例子,如果在 JavaScript 文件中的同一个作用域中有两个同名的 const 声明, 那么 TypeScript 会报告一个错误。

  1. const foo = 1234;
  2. // ~~~
  3. // error: Cannot redeclare block-scoped variable 'foo'.
  4. // ...
  5. const foo = 5678;
  6. // ~~~
  7. // error: Cannot redeclare block-scoped variable 'foo'.

另外一个例子,TypeScript 会报告修饰符是否被正确地使用了。

  1. function container() {
  2. export function foo() {
  3. // ~~~~~~
  4. // error: Modifiers cannot appear here.
  5. }
  6. }

这些检查可以通过在文件顶端添加 // @ts-nocheck 注释来禁用, 但是我们很想听听在大家的 JavaScript 工作流中使用该特性的反馈。 你可以在 Visual Studio Code 安装 TypeScript 和 JavaScript Nightly 扩展 来提前体验, 并阅读 PR1PR1

TypeScript Trace 分析器

有人偶尔会遇到创建和比较类型时很耗时的情况。 TypeScript 提供了一个 --generateTrace 选项来帮助识别耗时的类型, 或者帮助诊断 TypeScript 编译器中的问题。 虽说由 --generateTrace 生成的信息是非常有帮助的(尤其是在 TypeScript 4.6 的改进后), 但是阅读这些 trace 信息是比较难的。

近期,我们发布了 @typescript/analyze-trace 工具来帮助阅读这些信息。 虽说我们不认为每个人都需要使用 analyze-trace,但是我们认为它会为遇到了 TypeScript 构建性能问题的团队提供帮助。

更多详情请查看 repo