# TypeScript 4.6

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

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

class Base {
  // ...
}

class Derived extends Base {
  someProperty = true;

  constructor() {
    // 错误!
    // 必须先调用 'super()' 因为需要初始化 'someProperty'。
    doSomeStuff();
    super();
  }
}

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

感谢 Joshua Goldberg (opens new window)PR (opens new window)

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

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

type 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 是很常见的;然而,根据个人的喜好,你可能想对上例中的 kindpayload 进行解构。 就像下面这样:

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 会报错 - 当 kindpayload 是由同一个对象解构为变量时,它们会被独立对待。

在 TypeScript 4.6 中可以正常工作!

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

更多详情请查看 PR (opens new window)

# 改进的递归深度检查

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

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

interface 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'.
}

SourceTarget 的兼容性取决于它们的属性是否可以执行赋值操作。 此例中是指 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;

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

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

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

# 索引访问类型推断改进

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 时不再需要使用类型断言。

更多详情请阅读 PR (opens new window)

# 对因变参数的控制流分析

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

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' narrowed to 'number'
  }
  if (kind === 'b') {
    payload.toUpperCase(); // 'payload' narrowed to 'string'
  }
};

f1('a', 42);
f1('b', 'hello');

更多详情请阅读 PR (opens new window)

# --target es2022

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

感谢 Kagami Sascha Rosylight (saschanaz) (opens new window)实现 (opens new window)

# 删除 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 参数是没用的,删掉它会减小打包的体积。

感谢 https://github.com/a-tarasyuk (opens new window)PR (opens new window),TypeScript 4.6 会删除 void 0 参数。

# JSDoc 命名建议

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

/**
 * @param x The first operand
 * @param y The second operand
 */
function add(x, y) {
  return x + y;
}

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

/**
 * @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 文件上看到类似的提示! TypeScript 现在会给出建议,如果函数签名中的参数名与 JSDoc 中的参数名不一致。

example

改动 (opens new window)是由 Alexander Tarasyuk (opens new window) 提供的!

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

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

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

const foo = 1234;
//    ~~~
// error: Cannot redeclare block-scoped variable 'foo'.

// ...

const foo = 5678;
//    ~~~
// error: Cannot redeclare block-scoped variable 'foo'.

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

function container() {
  export function foo() {
    //  ~~~~~~
    // error: Modifiers cannot appear here.
  }
}

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

# TypeScript Trace 分析器

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

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

更多详情请查看 repo (opens new window)