TypeScript 4.6 正式发布[2022.02.28][官文全文翻译]

首发知乎:https://zhuanlan.zhihu.com/p/473952171

今天,我们发布了 TypeScript 4.6.

原文链接:https://devblogs.microsoft.com/typescript/announcing-typescript-4-6/

作者:Daniel

原文日期:2022.02.28

全文:10001 字。阅读时间 30 分钟。

今天,我们发布了 TypeScript 4.6.

如果你还不熟悉 TypeScript,TypeScript 是在 JavaScript 之上构建的一个编程语言,并且为 JavaScript 提供了类型的语法。类型帮助你知道你的代码的变量和函数的种类。TypeScript 可以利用这些信息,帮助你消除拼写错误,或者是不小心忘记的 null 和 undefined 的检查。但是 TypeScript 提供的远比这些多,TypeScript 可以用这些信息极大的提升你的开发体验,提供例如代码补全,跳转定义,重命名等功能。如果你已经用 Visual Studio 或者 Visual Studio Code 进行编写 JavaScript 的项目,你其实已经间接使用了 TypeScript!

开始使用 TypeScript,你可以通过 NuGet,或者通过下面这个命令:

npm install typescript

你可以通过编辑器支持:

  • 下载 Visual Studio 2022/2019
  • 安装 Visual Studio Code 或者根据文档去使用更新版本的 TypeScript
  • 在 Sublime Text 3 里使用包管理工具

如果你已经读了我们Beta 版本或者 RC 版本的博文,你可以直接看本次发布的变化部分。

下面是 TypeScript 4.6 新增的部分:

  • 允许 class 的构造函数内 super() 前执行代码
  • 对于解构 Discriminated 联合类型的控制流分析引擎(Control Flow Analysis)增强
  • 增强递归深度检查
  • 索引访问推断增强
  • Dependent 参数的控制流分析引擎增强
  • -target es2022
  • 移除 react-jsx 不必要的参数
  • JSDoc 名字建议
  • 对于 JavaScript 提供更多的语法和绑定错误提示
  • TypeScript Trace 分析工具
  • 重大改变

对于 Beta 和 RC 版本以来的变化

当我们宣布 beta,我们没有提两个重要的功能:对于 Destructured Discriminated Unions 的控制流分析引擎增强和增加 --target es2022。从 beta 版本以来,还有一个值得注意的变化是:我们移除了react-jsx 的 void 0参数。

距离我们 RC 版本的变化主要是,对于不匹配 JSDoc 参数名的提示。

距离 RC 版本,我们还修复了一些 issues,修复了一些奇怪的报错信息,增强了某些场景大约 3% 的类型检查的速度。你可以在这里读到更详细的情况。

允许 class 的构造函数内 super() 前执行代码

在 JavaScript 中,你必须在调用 this 前强制执行 super()。TypeScript 也强制执行了这个约定,但是我们实现这个限制用了过于严格的限制。在之前的版本,如果在 super() 前调用任何代码都会报错:

class Base {
  // ...
}

class Derived extends Base {
  someProperty = true

  constructor() {
    // 报错!
    // have to call 'super()' first because it needs to initialize 'someProperty'.
    doSomeStuff()
    super()
  }
}

这对于检查调用 this 前必须调用 super()变得很容易,但是这样做,你连不用 this 的代码,也不可以写了。TypeScript 4.6 现在允许你可以在 super() 前写不含有 this 的代码。

感谢 Joshua Goldberg 的 PR。

解构 Discriminated 联合类型的控制流分析引擎增强

TypeScript 可以收束名为 discriminant 属性的类型。例如,在下面的代码片段,TypeScript 可以通过 kind 的值收束 action 的类型。

type Action =
  | { kind: 'NumberContents'; payload: number }
  | { kind: 'StringContents'; payload: string }

function processAction(action: Action) {
  if (action.kind === 'NumberContents') {
    // `action.payload` 是 number 类型.
    let num = action.payload * 2
    // ...
  } else if (action.kind === 'StringContents') {
    // `action.payload` 是 string 类型.
    const str = action.payload.trim()
    // ...
  }
}

这可以让我们用同一个 objects 存储不同类型的数据,但是需要手动添加一个字段,告诉 TypeScript,这个数据是什么。

这在使用 TypeScript 非常常见。但是,也许,你想更进一步,做下面这个例子,在条件判断前,提前对数据进行解构:

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 可以记住从 discriminated 联合类型里解构的类型。在合适的情况下,解构出来的类型的关联依然保持,所以在上面的例子里,对于 kind 的收束可以获得对应的 payload 的类型。

对于更详细的信息,可以查看这个 PR。

增强递归深度检查

TypeScript 因为基于一个结构类型系统,并且还要提供范型,所以遇到很多有趣的挑战。

在一个结构类型系统中,object 类型可以通过他们有的成员的类似是否匹配来判断是否兼容。

interface Source {
  prop: string
}

interface Target {
  prop: number
}

function check(source: Source, target: Target) {
  target = source
  // 报错!
  // 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 是否可以赋值给 Target

interface Source {
  prop: Source>
}

interface Target {
  prop: Target>
}

function check(source: Source, target: Target) {
  target = source
}

为了知道这个问题的答案,TypeScript 需要去检查 prop 的类型是否可以兼容。这带来另一个问题:Source> 和 Target> 的类型是否兼容呢?为了知道这个问题,TypeScript 又去检查 prop 的情况。就带来另一个递归检查,去检查 Source>> 和 Target>> 的情况。你会发现,这会一直迭代下去。

这里,TypeScript 需要一些启发式的方法,如果类型检查展开了足够的深度,TypeScript 就认为,这个类型有可能可以兼容。这一般来说,是可以的,但是遗憾的是,有下面的这样的例子:

interface Foo {
  prop: T
}

declare let x: Foo>>>>>
declare let y: Foo>>>>

x = y

一个人类读者很容易知道,x 和 y 是不兼容的。然而类型是深层嵌套的,这只是他们的声明方式。启发式检查并不知道这个声明方式,而是要一层层进行检查。

TypeScript 4.6 现在可以区分这样的例子,然后对于最后这个例子,可以给出正确的报错。由于目前 TypeScript 已经不担心显示书写的类型的假误报,TypeScript 可以在更早的时候知道一个无限展开的类型,这个给类型检查的提速也带来了很多好处。通过这次的优化,一些无限类型的库比如 redux-immutable,react-lazylog 和 yup 有 100% 类型检查的提速(时间减少 50%)。

这个提升,你应该已经享受到了,因为我们 cherry-picked 到 TypeScript 4.5.3 了,你可以这这里读到更详细的情况。

索引访问推断增强

TypeScript 现在可以正确推断索引访问类型,从而映射 mapped object 类型的成员:

interface TypeMap {
  number: number
  string: string
  boolean: boolean
}

type UnionRecord

= { [K in P]: { kind: K v: TypeMap[K] f: (p: TypeMap[K]) => void } }[P] function processRecord(record: UnionRecord) { record.f(record.v) } // 这个在之前会报错 - 现在是可以工作的! 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) 是合法的,但是之前的版本,这个是有问题的。

TypeScript 4.6 之后,你不需要在调用 processRecord 前手动去做类型断言。

更多信息请参考这里。

Dependent 参数的控制流分析引擎增强

一个函数的参数签名可以通过展开参数的 discriminated tuples 联合类型来声明。

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

这样声明了以后,参数的具体类型,依赖第一个参数的值。当第一个参数是 “str” 时,第二个参数就是 string,反之亦然。

现在这样的例子,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')

对于更多的信息,请参照这里。

-target es2022

TypeScript 的 --target 选项,现在可以使用 es2022 了。这代表着,现在 class 字段可以正确输出。并且一些新的内置函数都可以使用了,比如 Arrays 的 at(),Object 的 hasOwn,新的报错信息,都可以通过 --target 使用,或者 --lib es2022 来使用。

这个实现是Kagami Sascha Rosylight (saschanaz) 实现的,感谢他的贡献。

移除 react-jsx 不必要的参数

在之前的版本,当使用 --jsx react-jsx 时,下面的代码:

export const el = foo

会被 TypeScript 编译为

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 的 PR。

JSDoc 名字建议

在 JSDoc 里,你可以通过 @param 标签来标注参数的类型。

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

但是,如果这些注释过期了呢?比如我们吧 x 和 y 改名为 a 和 b。

/**
 * @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 注释进行一些提示。

Alexander Tarasyuk 提供了这个变化。

对于 JavaScript 提供更多的语法和绑定错误提示

TypeScript 对了 JavaScript 文件增加了很多语法和报错的提示。你现在通过 Visual Studio 或者 Visual Studio Code 可以看到这些新的报错,也可以通过 TypeScript 编译器跑 JavaScript 代码来看到这些信息(你都不需要增加 checkJs 或者 // @ts-check)。

例如,如果你声明两个同名 const 变量,TypeScript 就会给你报错。

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

// ...

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

另一个例子是,TypeScript 可以让你知道你的 Modifiers 是不是写错了。

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

你可以通过增加 // @ts-nocheck 来关闭这些报错,但是我们也想知道,这些改动,对于 JavaScript 工作流有什么作用,有任何问题,欢迎来反馈。你可以在 Visual Studio Code 里安装 TypeScript 和 JavaScript 夜间扩展,和读这两个文章。

TypeScript Trace 分析工具

现在经常会有非常耗费性能的类型,让整个类型检查很慢。TypeScript 现在有 --generateTrace 标签来输出这些昂贵的类型,也可以通过这个报告来诊断一些 TypeScript 编译器的 issue。虽然这些信息有用,但是很难读。所以现在增加了可视化的观看方法。

我们最近发布了一个工具叫 @typescript/analyze-trace ,你可以通过这个工具来看一个图表的展示方式。我们不期望所有人都需要 analyze-trace,但是我们认为,这给一些性能问题提供了工具。

对于更多的信息,请看analyze-tracetool’s repo

重大改变

解构对象时丢弃范型对象的不可展开成员

现在对象展开时,对于不可展开的成员,会把对应的类型丢弃:

class Thing {
  someProperty = 42

  someMethod() {
    // ...
  }
}

function foo(x: T) {
  let { someProperty, ...rest } = x

  // 之前是通过的,现在会报错!
  // Property 'someMethod' does not exist on type 'Omit'.
  rest.someMethod()
}

rest 之前会有 Omit 类型,因为 TypeScript 会严格分析被解构的类型。因为在真正展开的运行情况和这个类型是不相符的。所以,在 4.6 中,rest 的类型,会变为 Omit

这个也会在 this 的解构中生效。当使用 ...rest 对 this 进行解构时,unspredable 和 non-public 成员,都会被丢弃。

class Thing {
  someProperty = 42

  someMethod() {
    // ...
  }

  someOtherMethod() {
    let { someProperty, ...rest } = this

    // 之前是通过的,现在会报错!
    // Property 'someMethod' does not exist on type 'Omit'.
    rest.someMethod()
  }
}

对于更多的信息,请看这里。

JavaScript 文件会一直受到语法和绑定的报错

之前,TypeScript 会忽略大部分 JavaScript 得语法报错,防止与 TypeScript 的语法混淆。现在 TypeScript 可以对 JavaScript 得语法进行校验,比如不正确的修饰符, 重复的声明,还有更多的东西。通过使用 Visual Studio Code 或者 Visual Studio 就可以获得这些能力,你也可以通过 TypeScript 编译器来实现。

你可以通过在 // @ts-nocheck 在文件顶部来关闭一个文件的检查。

可以看第一个和第二个这个功能的实现来详细了解这个功能。

下一步?

我们希望这次发布可以给你的代码之旅带来更多的快乐。如果你对于下一次发布也感兴趣,可以阅读我们对于 TypeScript 4.7 的规划。

Happy Hacking!

– Daniel Rosenwasser and the TypeScript Team

你可能感兴趣的:(TypeScript 4.6 正式发布[2022.02.28][官文全文翻译])