查看原文
其他

TypeScript 4.9 发布!重点新特性解读 ~

ConardLi code秘密花园 2023-01-25

大家好,我是 ConardLi

11 月 1 日,TypeScript 4.9 发布了候选版本 (RC),直到稳定版发布基本上不会有太大变化了,本次带来的更新还是挺有意思的,下面我就跟大家来一起看一下~

新的 satisfies 操作符

在使用 TypeScript 类型推断的时候,有很多情况下会让我们面临两难的选择:我们即希望确保某些表达式能够匹配某些类型,但也希望保留这个表达式的特定类型用来类型推断。

比如下面的例子,我们定义了一个颜色选择对象:

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

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

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

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

const palette = {
    // 错别字:rad -> red
    rad: [25500],
    green: "#00ff00",
    blue: [00255]
};

所以我们可能会为 palette 定义一个类型,这样错别字就会被检测出来了:

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

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

const palette: Record<Colors, string | RGB> = {
    rad: [25500],
//  ~~~~ The typo is now correctly detected
    green: "#00ff00",
    blue: [00255]
};

但是这时候我们再调用 palette.red 的方法,你会发现 TS 的类型推断会出错:

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

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

const palette: Record<Colors, string | RGB> = {
    red: [25500],
    green: "#00ff00",
    blue: [00255]
};

// 'palette.red' "could" 的类型是 string | RGB ,所以它不一定存在 at 方法
const a = palette.red.at(0);

这就让我们陷入了两难的境地,我们用更严格了类型约束了写出 bug 的可能性,但是却失去了类型推断的能力。

satisfies 关键字就是用来解决这个问题的,它既能让我们验证表达式的类型是否与某个类型匹配,也可以保留基于值进行类型推断的能力:

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

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

const palette = {
    rad: [25500],
    // 可以捕获到错别字 rad
    green: "#00ff00",
    blue: [00255]
} satisfies Record<Colors, string | RGB>;

// 都可以调用
const a = palette.red.at(0);
const b = palette.green.toUpperCase();

in 操作符类型收窄优化

在日常开发中,我们经常需要处理一些在运行时不完全确定的值,比如我们现在有下面两个类型:

interface Duck {
  quack(): string;
}

interface Cat {
  miao(): string;
}

在实际使用过程中,TS 不能确定 value 是否是上面中哪一个类型,所以会抛出错误:

function main(value: Duck | Cat) {
  if (value.quack) { // roperty 'quack' does not exist on type 'Duck | Cat'.
    return value.quack;
  }
}

我们可能会使用 in 这样的关键字来实现简单的类型收窄:

function main(value: Duck | Cat) {
  if ('quack' in value) { 
    return value.quack;
  }
}

也可以实现一个更通用的类型守卫,可以参考我这篇文章:什么是鸭子🦆类型?

但是,这个写法的前提是我们用到的对象有明确的类型,如果这个对象的属性没有明确的类型呢?我们来看看下面这段 JavaScript 代码:

function tryGetPackageName(context) {
    const packageJSON = context.packageJSON;
    // 检查是否是个对象
    if (packageJSON && typeof packageJSON === "object") {
        // 检查是否存在一个字符串类型的 name 属性
        if ("name" in packageJSON && typeof packageJSON.name === "string") {
            return packageJSON.name;
        }
    }

    return undefined;
}

把这段代码重写为规范的 TypeScript,我们只需要定义一个 Context 类型,但是由于 packageJSON 没有明确的类型定义,再使用 in 进行类型收窄就有问题了:

interface Context {
    packageJSON: unknown;
}

function tryGetPackageName(context: Context) {
    const packageJSON = context.packageJSON;
    // 检查是否是个对象
    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;
}

这是因为 in 操作符只会严格收窄到实际定义被检查属性的类型,所以 packageJSON 的类型从 unknown 收窄到了 object ,而  object 类型上不存在 name 属性,就会引发报错。

TypeScript 4.9 优化了这个问题,in 操作符更加强大了,它会被收窄为被检查类型和 Record<"property-key-being-checked", unknown> 的交叉类型。。。

比如在上面的例子中,packageJSON 的类型会被收窄为 object & Record<"name",unknown>,这样我们直接访问 packageJSON.name 就没问题了!

interface Context {
    packageJSON: unknown;
}

function tryGetPackageName(context: Context): string | undefined {
    const packageJSON = context.packageJSON;
    // 检查是否是个对象
    if (packageJSON && typeof packageJSON === "object") {
        // 检查是否存在一个字符串类型的 name 属性
        if ("name" in packageJSON && typeof packageJSON.name === "string") {
            // 可以正常运行!
            return packageJSON.name;
        }
    }

    return undefined;
}

TypeScript 4.9 还加强了一些关于如何使用 in 操作符的检查,比如左侧要检查的属性必须是 string | number | symbol 类型,而右侧类型必须要可分配给 object

accessor 关键字支持

accessorECMAScript 中即将推出的一个类关键字,TypeScript 4.9 对它提供了支持:

class Person {
    accessor name: string;

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

accessor 关键字可以为该属性在运行时转换为一对 getset 访问私有支持字段的访问器:

class Person {
    #__name: string;

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

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

NaN 相等判断警告

NaN 是一个特殊的数值,代表 “非数字” ,在 JS 中它和任何值相比较都是 false,包括它自己:

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 浮点数的语言都有相同的行为;但 JavaScript 的主要数字类型就是浮点数,并且 JavaScript 中的数字解析为 NaN 还挺常见的,所以在代码中去比较值是否等于 NaN 的情况还挺普遍的。但是正确的做法应该是使用 Number.isNaN 函数来判断。假如你不知道这个问题,就可能引发一些 bug。

TypeScript 4.9 中,如果你直接用一些值和 NaN 相比较,会抛出错误并提示你使用 Number.isNaN

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

return 关键字的定义

在编辑器中,当你对 return 关键字运行 go-to-definition 时,TypeScript 现在会自动跳转到相应函数的顶部。这有助于我们快速了解 return 属于哪个函数。

另外,TypeScript 会将此功能扩展到更多关键字,例如 await、yield、switch、case、default 等等。

最后

TypeScript 团队最近还发布了 5.0 版本的迭代规划(https://github.com/microsoft/TypeScript/issues/51362),这将是 TypeScript 的又一个大的版本,其中包含了很多有趣的想法,还是挺值得期待的!

更多详细更新请查看 TypeScript 官方博客:https://devblogs.microsoft.com/typescript/announcing-typescript-4-9-rc/

你觉得上面哪些更新对你最有用呢?欢迎在评论区和我留言;如果这篇文章帮助到了你,欢迎点赞和关注。

如果你想加入高质量前端交流群,或者你有任何其他事情想和我交流也可以添加我的个人微信 ConardLi

抖音安全团队正在招聘前端,感兴趣请查看:抖音安全团队招聘前端工程师

点赞在看是最大的支持⬇️❤️⬇️

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存