Intensive reading of "Typescript 4.4"

Typescript 4.4 is officially released! There are still three months before Typescript 4.5 is released, hurry up and learn!

Intensive article of the week: announcing-typescript-4-4

Overview

Smarter automatic type narrowing

The type narrowing function is very convenient, it allows Typescript to automatically and intelligently determine the type like Js, so as to avoid the work of type definition and make your Typescript write more like Js.

In fact, this function has existed for a long time. It has been introduced in our intensive reading of "Typescript2.0 - 2.9". The noun used at that time was automatic type inference. This time, the more precise word automatic type narrowing is used, because only type narrowing Narrow is safe, like:

function foo(arg: unknown) {
    if (typeof arg === "string") {
        // We know 'arg' is a string now.
        console.log(arg.toUpperCase());
    }
}
复制代码

Before Typescript 4.4, if we assign this judgment to a variable and use it in the ifbranch , the type cannot be narrowed normally:

function foo(arg: unknown) {
    const argIsString = typeof arg === "string";
    if (argIsString) {
        console.log(arg.toUpperCase());
        //              ~~~~~~~~~~~
        // Error! Property 'toUpperCase' does not exist on type 'unknown'.
    }
}
复制代码

This problem was solved in Typescript 4.4, which actually deepened the judgment logic of this type of narrowing, that is, no matter where the judgment is written, it can take effect. So the following usage judgment of destructuring can also infer type narrowing:

type Shape =
    | { kind: "circle", radius: number }
    | { kind: "square", sideLength: number };

function area(shape: Shape): number {
    // Extract out the 'kind' field first.
    const { kind } = shape;

    if (kind === "circle") {
        // We know we have a circle here!
        return Math.PI * shape.radius ** 2;
    }
    else {
        // We know we're left with a square here!
        return shape.sideLength ** 2;
    }
}
复制代码

Not only single judgment, Typescript 4.4 also supports composite type deduction:

function doSomeChecks(
    inputA: string | undefined,
    inputB: string | undefined,
    shouldDoExtraWork: boolean,
) {
    const mustDoWork = inputA && inputB && shouldDoExtraWork;
    if (mustDoWork) {
        // We can access 'string' properties on both 'inputA' and 'inputB'!
        const upperA = inputA.toUpperCase();
        const upperB = inputB.toUpperCase();
        // ...
    }
}
复制代码

mustDoWorktrueThe branch for , means inputAthat inputBboth are narrowed to stringtype .

This kind of deep judgment is also reflected in the recalculation of a variable with type judgment, and the generated variable also has the function of type judgment:

function f(x: string | number | boolean) {
    const isString = typeof x === "string";
    const isNumber = typeof x === "number";
    const isStringOrNumber = isString || isNumber;
    if (isStringOrNumber) {
        x;  // Type of 'x' is 'string | number'.
    }
    else {
        x;  // Type of 'x' is 'boolean'.
    }
}
复制代码

As you can see, we can write Typescript almost like writing Js, and 4.4 supports most of the intuitive derivations, which is very convenient. But it should be noted that Typescript is not a runtime after all, so it cannot do more thorough automatic inference, but it is enough to support most scenarios.

Subscript supports Symbol and template string type determination

Originally we defined an object accessed by subscript like this:

interface Values {
  [key: string]: number
}
复制代码

Symbol pulls are now also supported:

interface Colors {
    [sym: symbol]: number;
}

const red = Symbol("red");
const green = Symbol("green");
const blue = Symbol("blue");

let colors: Colors = {};

colors[red] = 255;          // Assignment of a number is allowed
let redVal = colors[red];   // 'redVal' has the type 'number'

colors[blue] = "da ba dee"; // Error: Type 'string' is not assignable to type 'number'.
复制代码

而且对于特定的字符串模版也支持类型匹配,比如希望以 data- 开头的下标是一种独立类型,可以这么定义:

interface Options {
    width?: number;
    height?: number;
}

let a: Options = {
    width: 100,
    height: 100,
    "data-blah": true, // Error! 'data-blah' wasn't declared in 'Options'.
};

interface OptionsWithDataProps extends Options {
    // Permit any property starting with 'data-'.
    [optName: `data-${string}`]: unknown;
}

let b: OptionsWithDataProps = {
    width: 100,
    height: 100,
    "data-blah": true,       // Works!

    "unknown-property": true,  // Error! 'unknown-property' wasn't declared in 'OptionsWithDataProps'.
};
复制代码

这个对于 HTML 的 data- 属性非常有帮助。

同时还支持联合类型定义,下面两种类型定义方式是等价的:

interface Data {
    [optName: string | symbol]: any;
}

// Equivalent to

interface Data {
    [optName: string]: any;
    [optName: symbol]: any;
}
复制代码

更严格的错误捕获类型

unknown 类型出来之前,Typescript 以 any 作为抛出错误的默认类型,毕竟谁也不知道抛出错误的类型是什么:

try {
    // Who knows what this might throw...
    executeSomeThirdPartyCode();
}
catch (err) { // err: any
    console.error(err.message); // Allowed, because 'any'
    err.thisWillProbablyFail(); // Allowed, because 'any' :(
}
复制代码

Who knows what this might throw... 这句话很有意思,一个函数任何地方都可能出现运行时错误,这根本不是静态分析可以解决的,所以不可能自动推断错误类型,所以只能用 any

在 Typescript 4.4 的 --useUnknownInCatchVariables--strict 模式下都将以 unknown 作为捕获到错误的默认类型。

相比不存在的类型 neverunknown 仅仅是不知道是什么类型而已,所以不能像 any 一样当作任何类型使用,但我们可以将其随意推断为任意类型:

try {
    executeSomeThirdPartyCode();
}
catch (err) { // err: unknown
    // Error! Property 'message' does not exist on type 'unknown'.
    console.error(err.message);

    // Works! We can narrow 'err' from 'unknown' to 'Error'.
    if (err instanceof Error) {
        console.error(err.message);
    }
}
复制代码

如果觉得这样做麻烦,也可以重新申明类型为 any

try {
    executeSomeThirdPartyCode();
}
catch (err: any) {
    console.error(err.message); // Works again!
}
复制代码

但这样做其实并不合适,因为即便是考虑了运行时因素,理论上还是可能发生意外错误,所以对错误过于自信的类型推断是不太合适的,最好保持其 unknown 类型,对所有可能的边界情况做处理。

明确的可选属性

对象的可选属性在类型描述时有个含糊不清的地方,比如:

interface Person {
    name: string,
    age?: number;
}
复制代码

其实 Typescript 对其的类型定义的是:

interface Person {
    name: string,
    age?: number | undefined;
}
复制代码

为什么要这么定义呢?因为很多情况下,没有这个 key,与这个 key 的值为 undefined 的表现是等价的。但比如 Object.keys 场景下这两种表现却又不等价,所以理论上对于 age?: number 的确切表述是:要么没有 age,要么有 age 且类型为 number,也就是说下面的写法应该是错误的:

// With 'exactOptionalPropertyTypes' on:
const p: Person = {
    name: "Daniel",
    age: undefined, // Error! undefined isn't a number
};
复制代码

在 Typescript 4.4 中同时开启 --exactOptionalPropertyTypes--strictNullChecks 即可生效。

仔细想想这是合理的,既然定义的类型不是 undefined,就算对象是可选类型,也不能认为赋值 undefined 是合理的,因为 age?: number 的心理预期是,要么没有这个 key,要么有但是类型为 number,所以当 Object.keys 发现 age 这个 key 时,值就应该是 number

支持 Static Block

Typescript 4.4 支持了 class static blocks,并且在代码块作用域内可以访问私有变量。

还有一些性能提升与体验优化杂项就不一一列举了,感兴趣可以直接看原文档:perf-improvements

总结

从 Typescript 4.4 特性可以看出,Typescript 正在往 “更具备原生 JS 亲和性” 方向作出努力,这无疑会使 Typescript 变得越来越好用。

对更多新特性感兴趣,可以 查看 Typescript 4.5 版本发布计划

The discussion address is: Intensive Reading "Typescript 4.4" Issue #348 dt-fe/weekly

If you'd like to join the discussion, click here , there are new topics every week, with a weekend or Monday release. Front-end intensive reading - help you filter reliable content.

Follow Front -end Intensive Reading WeChat Official Account

Copyright notice: Free to reprint - non-commercial - non-derivative - keep attribution ( Creative Commons 3.0 license )

Guess you like

Origin juejin.im/post/7002035805057187854