Typescript 4.4 正式发布了！距离 Typescript 4.5 发布还有三个月的时间，抓紧上车学习吧！

本周精读的文章：[announcing-typescript-4-4](https://devblogs.microsoft.com/typescript/announcing-typescript-4-4/)

## 概述

### 更智能的自动类型收窄

类型收窄功能非常方便，它可以让 Typescript 尽可能的像 Js 一样自动智能判定类型，从而避免类型定义的工作，让你的 Typescript 写得更像 Js。

其实这个功能早就有了，在我们 [精读《Typescript2.0 - 2.9》](https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/58.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript2.0%20-%202.9%E3%80%8B.md#%E8%87%AA%E5%8A%A8%E7%B1%BB%E5%9E%8B%E6%8E%A8%E5%AF%BC) 就已经介绍过，当时用的名词是自动类型推导，这次用了更精确的自动类型收窄一词，因为只有类型收窄是安全的，比如：

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

而在 Typescript 4.4 之前的版本，如果我们将这个判定赋值给一个变量，再用到 `if` 分支里，就无法正常收窄类型了：

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

这个问题在 Typescript 4.4 得到了解决，实际上是把这种类型收窄判断逻辑加深了，即无论这个判断写在哪都可以生效。所以下面这种解构的用法判断也可以推断出类型收窄：

```typescript
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;
    }
}
```

不仅是单一的判断，Typescript 4.4 还支持复合类型推导：

```typescript
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();
        // ...
    }
}
```

`mustDoWork` 为 `true` 的分支就意味着 `inputA`、`inputB` 均收窄为 `string` 类型。

这种深层的判定还体现在，一个具备类型判断的变量进行再计算，生成的变量还具有类型判断功能：

```typescript
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'.
    }
}
```

可以看到，我们几乎可以像写 Js 一样写 Typescript，4.4 支持了大部分符合直觉的推导非常方便。但要注意的是，Typescript
毕竟不是运行时，无法做到更彻底的自动推断，但足以支持绝大部分场景。

### 下标支持 Symbol 与模版字符串类型判定

原本我们定义一个用下标访问的对象是这样的：

```typescript
interface Values {
  [key: string]: number
}
```

现在也支持 Symbol 拉：

```typescript
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-` 开头的下标是一种独立类型，可以这么定义：

```typescript
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-` 属性非常有帮助。

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

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

// Equivalent to

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

### 更严格的错误捕获类型

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

```typescript
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` 作为捕获到错误的默认类型。

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

```typescript
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`：

```typescript
try {
    executeSomeThirdPartyCode();
}
catch (err: any) {
    console.error(err.message); // Works again!
}
```

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

### 明确的可选属性

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

```typescript
interface Person {
    name: string,
    age?: number;
}
```

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

```typescript
interface Person {
    name: string,
    age?: number | undefined;
}
```

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

```typescript
// 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](https://github.com/tc39/proposal-class-static-block#ecmascript-class-static-initialization-blocks)，并且在代码块作用域内可以访问私有变量。


还有一些性能提升与体验优化杂项就不一一列举了，感兴趣可以直接看原文档：[perf-improvements](https://devblogs.microsoft.com/typescript/announcing-typescript-4-4/#perf-improvements)。


## 总结

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

对更多新特性感兴趣，可以 [查看 Typescript 4.5 版本发布计划](https://github.com/microsoft/TypeScript/issues/45418)。

> 讨论地址是：[精读《Typescript 4.4》· Issue #348 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/348)

**如果你想参与讨论，请 [点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**

> 关注 **前端精读微信公众号**

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

> 版权声明：自由转载-非商用-非衍生-保持署名（[创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)）
