百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

TypeScript 4.9 正式发布「2022.11.15」「官文全文翻译」

toyiye 2024-08-19 01:06 20 浏览 0 评论

作者:Daniel Rosenwasser

原文日期:2022.11.15

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

今天我们很高兴地发布 TypeScript 4.9。

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

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

npm install -D typescript

你通过以下方式获取编辑器:

  • 下载 Visual Studio 2022/2019
  • 安装 Visual Studio Code 或者根据文档去使用更新版本的 TypeScript

这里是 TypeScript 4.9 更新的内容

  • satifies 操作符
  • in 操作符中未列举的属性收束
  • Class 的 Auto-Accessor
  • 对于 NaN 进行检查
  • 通过文件系统事件检测文件
  • 编辑器增强:“Remove Unused Imports” 和 “Sort Imports”
  • 编辑器增强:对于 return 关键字的 Go-to-Definition
  • 性能增强
  • 正确性修复和破坏性改变

从 Beta 和 RC 版本依赖的更新

相比 RC 版本,没有更多的变化加入到 TypeScript 4.9。Beta 版本本来包括 Class 的 Auto-Accessor 性能改进的部分,但是没有列出在文档上。

satisfies 操作符

TypeScript 开发者经常面对这样一个难题:我们需要保证一些表达式匹配一些类型,但是又希望保留这个类型更具体的形状。

例如:

// 每一个属性,既可以是RGB 元组,也可以是一个字符串。
const palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    bleu: [0, 0, 255]
//  ^^^^ sacrebleu - 这里故意写错了!
};

// 我们对于 red 使用数组的方法...
const redComponent = palette.red.at(0);

// 或者对于 green 使用 string 的方法...
const greenNormalized = palette.green.toUpperCase();

注意这里故意写成了 bleu, 而实际上应该写成 blue。 我们可以通过给 palette 加一个类型标准来避免这种问题, 但是这样我们又失去具体一个属性的具体类型了。

type Colors = "red" | "green" | "blue";

type RGB = [red: number, green: number, blue: number];

const palette: Record<Colors, string | RGB> = {
    red: [255, 0, 0],
    green: "#00ff00",
    bleu: [0, 0, 255]
//  ~~~~ 写错就会报错
};

// 但是 red 可能是一个 string,而在上面的表达式中,实际是一个数组。
const redComponent = palette.red.at(0);

新的 satisfies 关键字就是用来解决这个问题的。satisfies 可以用来校验一个更具体的形状是否符合预设的形状。我们用 satisfies 来解决上面的问题。

type Colors = "red" | "green" | "blue";

type RGB = [red: number, green: number, blue: number];

const palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    bleu: [0, 0, 255]
//  ~~~~ 写错就会报错,satisfies 会去校验!
} satisfies Record<Colors, string | RGB>;

// 但是!
const redComponent = palette.red.at(0);
const greenNormalized = palette.green.toUpperCase();

(Hugo 注:satisfies 能实现编译态和运行态更一致的类型关系,satisfies 有点像 ts 的 ts)

satisfies 可以用来捕获很多可能的错误。例如,我们可以保证一个 object 的 key 只能是有限的集合中的结果。

type Colors = "red" | "green" | "blue";

// 保证所有的 key 都来自 'Colors'.
const favoriteColors = {
    "red": "yes",
    "green": false,
    "blue": "kinda",
    "platypus": false
//  ~~~~~~~~~~ error - "platypus" 并不在 'Colors' 中.
} satisfies Record<Colors, unknown>;

//所有关于 'red', 'green', 和 'blue' 的属性信息都是和值声明一致的.
const g: boolean = favoriteColors.green;

也许我们不关心 object 的 key 因为名称,而更关心 value 的类型。在下面的例子中,也可以解决这样的问题:

type RGB = [red: number, green: number, blue: number];

const palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    blue: [0, 0]
    //    ~~~~~~ error!
} satisfies Record<string, string | RGB>;

// 所有值的类型和上面的值声明是一致的.
const redComponent = palette.red.at(0);
const greenNormalized = palette.green.toUpperCase();

如果你想看更多的例子,可以查看 issue 和 pr。我们感谢 Oleksandr Tarasiuk 实现了这个功能。

in 操作符中未列举的属性收束

作为开发者,我们经常需要处理程序运行时不完全知道的类型。事实上,我们从服务器或者配置文件读一个数据,并不能完全确定这个属性是否存在,JavaScript 的 in 操作符提供了检查一个字段是否存在的手段。

在之前,TypeScript 也提供了一定的对使用 in 操作符进行类型收束。

interface RGB {
    red: number;
    green: number;
    blue: number;
}

interface HSV {
    hue: number;
    saturation: number;
    value: number;
}

function setColor(color: RGB | HSV) {
    if ("hue" in color) {
        // 'color'd HSV
    }
    // ...
}

类型 RGB 并没有 hue 字段,所以可以进行类型收束,在 in 的 block 中,类型被收束为 HSV。

但是,如果没有进行类型标准,会变成什么样子呢?

function tryGetPackageName(context) {
    const packageJSON = context.packageJSON;
    // 检查我们收到的类型是一个 object.
    if (packageJSON && typeof packageJSON === "object") {
        // 检查存在 name 字段.
        if ("name" in packageJSON && typeof packageJSON.name === "string") {
            return packageJSON.name;
        }
    }

    return undefined;
}

把上面的例子改写为 TypeScript,并使用 unknown 类型。

interface Context {
    packageJSON: unknown;
}

function tryGetPackageName(context: Context) {
    const packageJSON = context.packageJSON;
    // 检查我们收到的类型是一个 object.
    if (packageJSON && typeof packageJSON === "object") {
        // 检查存在 name 字段.
        if ("name" in packageJSON && typeof packageJSON.name === "string") {
        //                                              ~~~~
        // error! Property 'name' does not exist on type 'object.
            return packageJSON.name;
        //                     ~~~~
        // error! Property 'name' does not exist on type 'object.
        }
    }

    return undefined;
}

这里会报错是因为,在之前的版本,虽然 unkown 被收束为 object,但是之后的收束并没有生效,TypeScript 依然认为 packageJSON 只是一个 object,而不知道有 name 这个字段。

TypeScript 4.9 会更智能,在通过 in 操作符以后,会给类型添加上断言添加的类型 Record<"property-key-being-checked", unknown>。

所以,在 TypeScript 4.9 中,packageJSON 的类型会先从 unknown 收束为 object,然后继续收束为 object & Record<"name", unknown>。这样后续的操作就知道 packageJSON 有 name 这个字段。

interface Context {
    packageJSON: unknown;
}

function tryGetPackageName(context: Context): string | undefined {
    const packageJSON = context.packageJSON;
    // 检查我们收到的类型是一个 object.
    if (packageJSON && typeof packageJSON === "object") {
        // 检查存在 name 字段.
        if ("name" in packageJSON && typeof packageJSON.name === "string") {
            // 不会报错了!
            return packageJSON.name;
        }
    }

    return undefined;
}

TypeScript 也会对 in 操作符两端做检查,确保左边是 string | number | symbol, 右边是 object。这会保证我们检查的左边是合法的 key,而右边不是在检查一个基础类型。

更多的信息请查看 pr。

(Hugo 注,这个功能虽然简单,但是让 TypeScript 的断言能力进一步提升,在核心关键点写出更安全的代码提供了方便。)

Auto-Accessors in Classes

TypeScript 只吃了 ECMAScript 的新功能 auto-accessors。auto-accessors 就和 class 的属性一样, ch

class Person {
    accessor name: string;

    constructor(name: string) {
        this.name = name;
    }
}

上面这个写法,在最后会被去糖味 get 和 set 以及不可访问的原生私有属性。

class Person {
    #__name: string;

    get name() {
        return this.#__name;
    }
    set name(value: string) {
        this.#__name = name;
    }

    constructor(name: string) {
        this.name = name;
    }
}

对这个功能关心的话,请查看 pr。

对于 NaN 进行检查

对于 JavaScript 开发者来说,检查一个值和 NaN 的关系是一件不容易的事。

NaN 是一个特殊的数字型值,表示 “不是一个数字”。什么值和 NaN 都不相等,包括 NaN 自己。

console.log(NaN == 0)  // false
console.log(NaN === 0) // false

console.log(NaN == NaN)  // false
console.log(NaN === NaN) // false

和这个等价的另一个规则是,任何东西都和 NaN 不相等。

console.log(NaN != 0)  // true
console.log(NaN !== 0) // true

console.log(NaN != NaN)  // true
console.log(NaN !== NaN) // true

这个奇怪的行为并不是 JavaScript 独有的,任何语言只要实现了 IEEE-754 floats 标准,就会有这个行为。但是 JavaScript 的原生数字类型是一个浮点数型数字值,并且 JavaScript 的数字解析经常会出现 NaN。检查和 NaN 在处理数字相关的代码时,是非常常见的。通常使用 Number.isNaN,但是就像上面提到的,很多开发者实际使用 someValue === NaN 来实现这个功能。

TypeScript 会对 NaN 的直接比较进行报错,提示开发者使用 Number.isNaN(Hugo 注:多么贴心的功能。)。

function validate(someValue: number) {
    return someValue !== NaN;
    //     ~~~~~~~~~~~~~~~~~
    // error: This condition will always return 'true'.
    //        Did you mean '!Number.isNaN(someValue)'?
}

我们认为这个改变能帮助新手开发者防止错误,就像 TypeScript 目前不可以比较 object 和 array 一样。

感谢 Oleksandr Tarasiuk 贡献了这个 PR。

通过文件系统事件检测文件

在早期的版本里,TypeScript 非常依赖轮训来检测单个文件。使用轮训的机制表示,TypeScript 需要周期的检查一个文件。在 Node.js 里, fs.watchFIle 时内置的获取轮训文件检测器的内置方法。因为轮训的机制在不同的平台和文件系统中是比较确定的,它会时不时终端 CPU 来看这个文件的状态,即便这个文件啥也没做,也要发生中断。如果文件不多,这个机制是合适的。但是如果文件特别多,比如 node_modules 里的那么多文件,这种机制会造成一些资源占用浪费。

通常来说,比较好的方法是通过文件系统事件来实现上面的机制。不再使用轮训的机制,我们可以关注关心的文件,然后通过事件触发的回调来实现。绝大部分现代平台提供了CreateIoCompletionPort, kqueue, epoll, 和 inotify。Node.js 提供了[fs.watch](<https://nodejs.org/docs/latest-v18.x/api/fs.html#fswatchfilename-options-listener>) ,这个接口抽象了这些实现方式。使用 fs.watch 接口来使用文件系统事件通常工作很好,但是也有一些缺陷。一个检测者要小心考虑 inode watching,在一些文件系统不可用(比如网络文件系统)。是否有递归文件检测是可用的,文件夹改名是否触发事件, 还有文件检测者耗尽的问题。换句话说,使用这个机制,需要考虑非常多的问题,尤其是在跨平台使用时。

所以目前的解决方案时,我们默认的方法是在绝大部分时间使用轮训。

随着时间发展,我们会提供其他的文件检测机制。这让我们可以更多地获得关于跨平台碰到相关问题的反馈。因为 TypeScript 的项目会扩展为非常大的代码库,我们认为切换到基于文件事件的机制是值得投资的事情。

在 TypeScript 4.9, 文件检测默认使用文件系统事件,只有在设置事件检测者失败时回退成轮训的机制。对于绝大部份开发者,使用 —watch 模式可以消耗更少的资源,在使用 TypeScript 强化的编辑器例如 Visual Studio 或者 VS Code 时也会使用更少的资源。

使用 watchOptions 可以改变这个机制。VS Code 也提供了改变这个参数的方法。如果开发者使用网络文件系统(例如 NFS 和 SMB),需要把这个参数回退成轮训的机制,当然直接在服务器端使用 TypeScript 也是一个不错的选择,这样就是使用本地文件系统了。VS Code 有很多关于远程开发的插件来帮助这个过程。

你可以在这篇文章看到关于这个问题更多的信息。

编辑器增强:“Remove Unused Imports” 和 “Sort Imports”

在之前的版本,TypeScript 只支持两个编辑器命令来管理 import。 例如

import { Zebra, Moose, HoneyBadger } from "./zoo";
import { foo, bar } from "./helper";

let x: Moose | HoneyBadger = foo();

第一个时 “Organize Imports”,会把不使用的 imports 移除,然后对剩下的 import 进行排序。上面的文件会被重写为:

import { foo } from "./helper";
import { HoneyBadger, Moose } from "./zoo";

let x: Moose | HoneyBadger = foo();

在 TypeScript 4.3, 我们引入了 “Sort Import” 命令,可以只对文件进行排序,而不移除它们,使用这个功能会让一开始的代码变为

import { bar, foo } from "./helper";
import { HoneyBadger, Moose, Zebra } from "./zoo";

let x: Moose | HoneyBadger = foo();

使用 “Sort Imports” 的缺陷是,在 Visual Studio Code 中,这个功能只能是保存时调用的功能,而不是手动触发的功能。

TypeScript 4.9 增加了另一半功能,即 “Remove Unused Imports”,TypeScript 可以移除不使用的 import 和语句,把剩下的代码留下。

import { Moose, HoneyBadger } from "./zoo";
import { foo } from "./helper";

let x: Moose | HoneyBadger = foo();

这个功能对于全部编辑器可用,但是注意 Visual Studio Code(1.73 和之后)会支持内置的可以在命令面板调用的这些功能。用户如果想更细粒度地控制这个行为,可以混合调用 “Remove Unused Imports”、“Sort Imports” 和 “Organize Imports”。

更详细的文档请参考。

编辑器增强:对于 return 关键字的 Go-to-Definition

在编辑器中,当对 return 关键字执行 go-to-definition,TypeScript 会跳到相关函数的顶部。这对于知道这个 return 属于哪个函数是有帮助的。

我们期望 TypeScript 可以扩展这个行为到更多的关键字,比如 await 和 yield,switch、case 和 default。

感谢 Oleksandr Tarasiuk 提供了这个实现。

性能增强

TypeScript 有了一些小但是值得注意的性能增强。

首先,TypeScript 的 forEachChild 函数使用函数表查找重写了 switch 语句的实现。编译器在进行语法节点遍历时非常依赖 forEachChild,并且在语言服务器的编译器链接阶段也用的很重。重构 forEachChild 带来了绑定阶段大约 20% 的性能提升。

当我们最终发现这个优化对于 forEachChild 的实现很有效果,我们在 visitEachChild(这个函数在编译器和语言服务器中进行转换节点的工作)也做一样的优化。这样大概提升了 visitEachChild 3% 的性能。

最开始对于 forEachChild 优化的启发是来自Artemis Everfree 的博客。虽然我们认为目前速度的问题更多是函数的大小和复杂性有关,并不是博文中指出的问题,但是我们对于从这个经验中找到这个优化方法是非常感激的。

最后,对于 TypeScript 在条件分支中保留类型信息做了一些优化,对于类型

interface Zoo<T extends Animal> {
    // ...
}

type MakeZoo<A> = A extends Animal ? Zoo<A> : never;

TypeScript 在检查 Zoo<A> 是合法时需要知道 A 是一个 Animal。在之前的版本,TypeScript总是立即做了这件事,目前看是不必要的。并且,一些我们的类型检查器中的错误代码让我们无法简化这个过程。TypeScript 现在推迟到必须知道这个类型时再去检查类型。对于使用条件类型非常多的代码库,能看到非常大的性能提升,对于常规情况,我们看到 3% 的类型检查时间提升。

你可以阅读下面的 PR 来了解更详细的信息

  • [forEachChild as a jump-table](https://github.com/microsoft/TypeScript/pull/50225)
  • [visitEachChild as a jump-table](https://github.com/microsoft/TypeScript/pull/50266)
  • Optimize substitition types

正确性修复和破坏性改变

更新 lib.d.ts

虽然 TypeScript 尽量避免大的破坏式更新,因为内置库的一点小变化也会导致一些问题,但是关于 DOM 和 lib.d.ts 仍然会有一些小的破坏式更新。

对于 Promise.resolve 的类型增强

Promise.resolve 现在使用 Awaited 类型来对 Proimse-like 的类型进行解包。这意味着现在更多返回正确的 Promise 的类型,而不是 any 或者 unknown。更与这个变更更多请参考。

JavaScript不再触发省略 import

当 TypeScript 编译器开始支持 JavaScript 的类型检查和编译时,TypeScript 引入了一些机制,例如省略 import。这个功能的意思是,如果编译器发现一个引入的东西不作为值,则会在最终生成的文件省略这个 import。

现在,TypeScript 会保留这些 import。

// 输入:
import { someValue, SomeClass } from "some-module";

/** @type {SomeClass} */
let val = someValue;

// 之前版本的输出:
import { someValue } from "some-module";

/** @type {SomeClass} */
let val = someValue;

// 现在的输出:
import { someValue, SomeClass } from "some-module";

/** @type {SomeClass} */
let val = someValue;

更多关于这个内容的信息参考。

exports 优先级高于 typesVersions

在之前的版本中,当 TypeScript 解析 package.json 通过 --moduleResolution node16 时,TypeScript 会错误提升 typesVersions 的优先级高于 exports。如果这个改变影响你的库,你需要增加 types@ 字段。

{
      "type": "module",
      "main": "./dist/main.js"
      "typesVersions": {
          "<4.8": { ".": ["4.8-types/main.d.ts"] },
          "*": { ".": ["modern-types/main.d.ts"] }
      },
      "exports": {
          ".": {
+             "types@<4.8": "4.8-types/main.d.ts",
+             "types": "modern-types/main.d.ts",
              "import": "./dist/main.js"
          }
      }
  }

更多信息参考。(Hugo 注:这种类型的功能,建议等三个版本再上生产。)

对于 SubstitutionType 的 substitute 替换为 constraint

对于替换类型的优化,SubstitutionType 对象不在包含 substitute 属性,substitute 属性代表高效替换,通常是基础类型和隐式限制的交集。现在 SubstitutionType 值包含 constraint 属性。

更多信息,参考。

下一步

我们目前发布了了 5.0 版本的迭代计划,里面有很多有趣的功能!如果你感兴趣,我们期望你们能来看看。

期望 4.9 让你的代码旅途更快乐。

Happy Hacking!

– Daniel Rosenwasser and the TypeScript Team

相关推荐

# Python 3 # Python 3字典Dictionary(1)

Python3字典字典是另一种可变容器模型,且可存储任意类型对象。字典的每个键值(key=>value)对用冒号(:)分割,每个对之间用逗号(,)分割,整个字典包括在花括号({})中,格式如...

Python第八课:数据类型中的字典及其函数与方法

Python3字典字典是另一种可变容器模型,且可存储任意类型对象。字典的每个键值...

Python中字典详解(python 中字典)

字典是Python中使用键进行索引的重要数据结构。它们是无序的项序列(键值对),这意味着顺序不被保留。键是不可变的。与列表一样,字典的值可以保存异构数据,即整数、浮点、字符串、NaN、布尔值、列表、数...

Python3.9又更新了:dict内置新功能,正式版十月见面

机器之心报道参与:一鸣、JaminPython3.8的热乎劲还没过去,Python就又双叒叕要更新了。近日,3.9版本的第四个alpha版已经开源。从文档中,我们可以看到官方透露的对dic...

Python3 基本数据类型详解(python三种基本数据类型)

文章来源:加米谷大数据Python中的变量不需要声明。每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建。在Python中,变量就是变量,它没有类型,我们所说的"类型"是变...

一文掌握Python的字典(python字典用法大全)

字典是Python中最强大、最灵活的内置数据结构之一。它们允许存储键值对,从而实现高效的数据检索、操作和组织。本文深入探讨了字典,涵盖了它们的创建、操作和高级用法,以帮助中级Python开发...

超级完整|Python字典详解(python字典的方法或操作)

一、字典概述01字典的格式Python字典是一种可变容器模型,且可存储任意类型对象,如字符串、数字、元组等其他容器模型。字典的每个键值key=>value对用冒号:分割,每个对之间用逗号,...

Python3.9版本新特性:字典合并操作的详细解读

处于测试阶段的Python3.9版本中有一个新特性:我们在使用Python字典时,将能够编写出更可读、更紧凑的代码啦!Python版本你现在使用哪种版本的Python?3.7分?3.5分?还是2.7...

python 自学,字典3(一些例子)(python字典有哪些基本操作)

例子11;如何批量复制字典里的内容2;如何批量修改字典的内容3;如何批量修改字典里某些指定的内容...

Python3.9中的字典合并和更新,几乎影响了所有Python程序员

全文共2837字,预计学习时长9分钟Python3.9正在积极开发,并计划于今年10月发布。2月26日,开发团队发布了alpha4版本。该版本引入了新的合并(|)和更新(|=)运算符,这个新特性几乎...

Python3大字典:《Python3自学速查手册.pdf》限时下载中

最近有人会想了,2022了,想学Python晚不晚,学习python有前途吗?IT行业行业薪资高,发展前景好,是很多求职群里严重的香饽饽,而要进入这个高薪行业,也不是那么轻而易举的,拿信工专业的大学生...

python学习——字典(python字典基本操作)

字典Python的字典数据类型是基于hash散列算法实现的,采用键值对(key:value)的形式,根据key的值计算value的地址,具有非常快的查取和插入速度。但它是无序的,包含的元素个数不限,值...

324页清华教授撰写【Python 3 菜鸟查询手册】火了,小白入门字典

如何入门学习python...

Python3.9中的字典合并和更新,了解一下

全文共2837字,预计学习时长9分钟Python3.9正在积极开发,并计划于今年10月发布。2月26日,开发团队发布了alpha4版本。该版本引入了新的合并(|)和更新(|=)运算符,这个新特性几乎...

python3基础之字典(python中字典的基本操作)

字典和列表一样,也是python内置的一种数据结构。字典的结构如下图:列表用中括号[]把元素包起来,而字典是用大括号{}把元素包起来,只不过字典的每一个元素都包含键和值两部分。键和值是一一对应的...

取消回复欢迎 发表评论:

请填写验证码