Skip to content
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待
虚位以待

TypeScript 4.6

允许在 super() 之前编写构造函数代码

在 JavaScript 类中,必须在引用 this 之前调用 super()。 TypeScript 也强制执行这一点,但在如何确保这一点方面有点过于严格。 在 TypeScript 中,如果包含类有任何属性初始化器,则在构造函数开头包含任何代码以前是错误。

ts
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 的类型。

ts
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 中非常常见;然而,根据你的偏好,你可能希望在上面的示例中解构 kindpayload。 也许像下面这样:

ts
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 的类型。

有关更多信息,请查看实现此分析的拉取请求

改进的递归深度检查

由于 TypeScript 建立在同时提供泛型的结构类型系统之上,因此它面临一些有趣的挑战。

在结构类型系统中,对象类型基于它们具有的成员而兼容。

ts
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>

ts
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 在这里有一些启发式方法——如果一种类型在遇到一定深度检查后似乎无限扩展,那么它认为这些类型可能是兼容的。 这通常足够了,但令人尴尬的是,有一些假阴性是它无法捕获的。

ts
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 可以更早地得出结论,即类型无限扩展,并节省大量检查类型兼容性的工作。 因此,像 redux-immutablereact-lazylogyup 这样的 DefinitelyTyped 库的检查时间减少了 50%。

你可能已经拥有此更改,因为它被 cherry-pick 到了 TypeScript 4.5.3 中,但它是 TypeScript 4.6 的一个显著特性,你可以在此处阅读更多信息。

索引访问推断改进

TypeScript 现在可以正确推断立即索引到映射对象类型的索引访问类型。

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

有关更多信息,你可以阅读拉取请求

依赖参数的控制流分析

可以使用一个剩余参数来声明一个签名,其类型是元组的可辨识联合。

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

这意味着 func 的参数完全取决于第一个参数。 当第一个参数是字符串 "str" 时,它的第二个参数必须是 string。 当第一个参数是字符串 "num" 时,它的第二个参数必须是 number

在 TypeScript 从这样的签名推断函数类型的情况下,TypeScript 现在可以收窄相互依赖的参数。

ts
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.hasOwnnew Error 上的 cause 选项,可以与此新的 --target 设置一起使用,或与 --lib es2022 一起使用。

此功能由 Kagami Sascha Rosylight (saschanaz) 在多个 PR 中实现,我们感谢这一贡献!

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

以前,在 --jsx react-jsx 下编译如下代码时

tsx
export const el = <div>foo</div>;

TypeScript 会生成以下 JavaScript 代码:

jsx
import { 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 第一个操作数
 * @param y 第二个操作数
 */
function add(x, y) {
  return x + y;
}

但是当这些注释过时时会发生什么? 如果我们把 xy 重命名为 ab 呢?

js
/**
 * @param x {number} 第一个操作数
 * @param y {number} 第二个操作数
 */
function add(a, b) {
  return a + b;
}

以前,TypeScript 只在执行 JavaScript 文件的类型检查时才会告诉你这一点——当使用 checkJs 选项,或在文件顶部添加 // @ts-check 注释时。

现在你可以在编辑器中为 TypeScript 文件获得类似的信息! 当参数名称在你的函数与其 JSDoc 注释之间不匹配时,TypeScript 现在会提供建议。

编辑器中显示的 JSDoc 注释中参数名称与实际参数名称不匹配的建议诊断。

此更改Alexander Tarasyuk 提供!

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

TypeScript 扩展了其在 JavaScript 文件中的语法和绑定错误集。 如果你在 Visual Studio 或 Visual Studio Code 等编辑器中打开 JavaScript 文件,或者通过 TypeScript 编译器运行 JavaScript 代码,你会看到这些新错误——即使你没有打开 checkJs 或在文件顶部添加 // @ts-check 注释。

例如,如果你在 JavaScript 文件的同一作用域中有两个 const 声明,TypeScript 现在会对这些声明发出错误。

ts
const foo = 1234;
//    ~~~
// 错误:无法重新声明块作用域变量 'foo'。

// ...

const foo = 5678;
//    ~~~
// 错误:无法重新声明块作用域变量 'foo'。

再比如,TypeScript 会告诉你修饰符是否被错误使用。

ts
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 工具的存储库

破坏性更改

对象剩余表达式从泛型对象中删除不可展开的成员

对象剩余表达式现在从泛型对象中删除看起来不可展开的成员。 在以下示例中...

ts
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 时,不可展开和非公共成员现在被丢弃,这与在其他地方解构类的实例一致。

ts
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 注释来显式关闭这些错误。

有关更多信息,请参阅这些功能的第一第二个实现拉取请求。