zl程序教程

您现在的位置是:首页 >  前端

当前栏目

Typescript 4.9重点特性探索

typescript 探索 特性 重点 4.9
2023-06-13 09:13:15 时间

toc

这里是 TypeScript 4.9 更新的部分内容

  • satifies 操作符
  • in操作符中未列举的属性收束
  • Class 的 Auto-Accessor
  • 对于 NaN 进行检查
  • 编辑器增强:“Remove Unused Imports” 和 “Sort Imports”
  • 编辑器增强:对于 return 关键字的 Go-to-Definition

satisfies 操作符

TypeScript 开发者可能遇到的一个问题:既要确保表达式匹配某些类型,又要保留该表达式的具体类型。

比如我们定义一个颜色对象

const palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    blue: [0, 0, 255]
};

因为每个属性都被赋予了默认值, ts 会自动帮我们自动推导 palette 的属性类型,所以我们可以直接调用它们的方法:

const a = palette.red.at(0); // red 被推断为 number[] 类型
const b = palette.green.toUpperCase(); // green 被推断为 string 类型

由于颜色都是固定的,我们想让我们的 palette 对象拥有特定的几个属性,来避免我们写出一些错别字:

const palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    bleu: [0, 0, 255] // 故意写错
};

当我们为 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] //  ~~~~ The typo is now correctly detected
};

这时候我们再调用 palette.blue 的方法,这时ts的类型推断会出错:

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",
    blue: [0, 0, 255]
};
 
// 'palette.blue' "could" 的类型是 string | RGB ,所以它不一定存在 at 方法
const a = palette.blue.at(0);

如此就暴露出一个问题,我们用更严格的类型约束了写出bug的可能性,但是却失去了类型推断的能力。

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] // 可以捕获到错别字bleu
} satisfies Record<Colors, string | RGB>;
// 以下两种方法都可以调用
const a = palette.bleu.at(0);
const b = palette.bleu.toUpperCase();

由此我们可以看到新增 satisfies 操作符,类似于 as,但他更像一个不那么 strictas

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

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

在之前,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;
}

把上面的例子改写为ts,并使用 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;
}

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

TypeScript4.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,而右边不是在检查一个基础类型。

这个功能虽然简单,但是让 TypeScript 的断言能力进一步提升,为开发者写出更安全的代码提供了方便。

Auto-Accessors in Classes

TypeScript 4.9 支持 ECMAScript中即将推出的功能,称为自动访问器,自动访问器的声明就像类的属性一样,只是它们用 accessor关键字声明

class Person {
    accessor name: string;

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

类的自动访问器会转化为具有无法访问的私有属性的获取和设置访问器。

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来实现这个功能,这样就会引发一些不必要的bug

TypeScript4.9会对NaN的直接比较进行报错,提示开发者使用Number.isNaN函数。

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

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

编辑器增强:“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();

TypeScript4.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 和 yieldswitch、case 和 default