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

你可以通过编辑器支持:

如果你已经读了我们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 GoldbergPR

解构 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<string> 是否可以赋值给 Target<number>?</number></string>

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>>> 的情况。你会发现,这会一直迭代下去。</number></string></number></string>

这里,TypeScript 需要一些启发式的方法,如果类型检查展开了足够的深度,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 已经不担心显示书写的类型的假误报,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<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' 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 TarasyukPR

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<T extends Thing>(x: T) {
  let { someProperty, ...rest } = x

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

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

这个也会在 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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容