## 1 引言

随着 [Typescript 4 Beta](https://devblogs.microsoft.com/typescript/announcing-typescript-4-0-beta/) 的发布，又带来了许多新功能，其中 Variadic Tuple Types 解决了大量重载模版代码的顽疾，使得这次更新非常有意义。

## 2 简介

### 可变元组类型

考虑 `concat` 场景，接收两个数组或者元组类型，组成一个新数组：

```typescript
function concat(arr1, arr2) {
  return [...arr1, ...arr2];
}
```

如果要定义 `concat` 的类型，以往我们会通过枚举的方式，先枚举第一个参数数组中的每一项：

```typescript
function concat<>(arr1: [], arr2: []): [A];
function concat<A>(arr1: [A], arr2: []): [A];
function concat<A, B>(arr1: [A, B], arr2: []): [A, B];
function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];
function concat<A, B, C, D>(arr1: [A, B, C, D], arr2: []): [A, B, C, D];
function concat<A, B, C, D, E>(arr1: [A, B, C, D, E], arr2: []): [A, B, C, D, E];
function concat<A, B, C, D, E, F>(arr1: [A, B, C, D, E, F], arr2: []): [A, B, C, D, E, F];)
```

再枚举第二个参数中每一项，如果要完成所有枚举，仅考虑数组长度为 6 的情况，就要定义 36 次重载，代码几乎不可维护：

```typescript
function concat<A2>(arr1: [], arr2: [A2]): [A2];
function concat<A1, A2>(arr1: [A1], arr2: [A2]): [A1, A2];
function concat<A1, B1, A2>(arr1: [A1, B1], arr2: [A2]): [A1, B1, A2];
function concat<A1, B1, C1, A2>(
  arr1: [A1, B1, C1],
  arr2: [A2]
): [A1, B1, C1, A2];
function concat<A1, B1, C1, D1, A2>(
  arr1: [A1, B1, C1, D1],
  arr2: [A2]
): [A1, B1, C1, D1, A2];
function concat<A1, B1, C1, D1, E1, A2>(
  arr1: [A1, B1, C1, D1, E1],
  arr2: [A2]
): [A1, B1, C1, D1, E1, A2];
function concat<A1, B1, C1, D1, E1, F1, A2>(
  arr1: [A1, B1, C1, D1, E1, F1],
  arr2: [A2]
): [A1, B1, C1, D1, E1, F1, A2];
```

如果我们采用批量定义的方式，问题也不会得到解决，因为参数类型的顺序得不到保证：

```typescript
function concat<T, U>(arr1: T[], arr2, U[]): Array<T | U>;
```

在 Typescript 4，可以在定义中对数组进行解构，通过几行代码优雅的解决可能要重载几百次的场景：

```typescript
type Arr = readonly any[];

function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] {
  return [...arr1, ...arr2];
}
```

上面例子中，`Arr` 类型告诉 TS `T` 与 `U` 是数组类型，再通过 `[...T, ...U]` 按照逻辑顺序依次拼接类型。

再比如 `tail`，返回除第一项外剩下元素：

```typescript
function tail(arg) {
  const [_, ...result] = arg;
  return result;
}
```

同样告诉 TS `T` 是数组类型，且 `arr: readonly [any, ...T]` 申明了 `T` 类型表示除第一项其余项的类型，TS 可自动将 `T` 类型关联到对象 `rest`：

```typescript
function tail<T extends any[]>(arr: readonly [any, ...T]) {
  const [_ignored, ...rest] = arr;
  return rest;
}

const myTuple = [1, 2, 3, 4] as const;
const myArray = ["hello", "world"];

// type [2, 3, 4]
const r1 = tail(myTuple);

// type [2, 3, ...string[]]
const r2 = tail([...myTuple, ...myArray] as const);
```

另外之前版本的 TS 只能将类型解构放在最后一个位置：

```typescript
type Strings = [string, string];
type Numbers = [number, number];

// [string, string, number, number]
type StrStrNumNum = [...Strings, ...Numbers];
```

如果你尝试将 `[...Strings, ...Numbers]` 这种写法，将会得到一个错误提示：

```text
A rest element must be last in a tuple type.
```

但在 Typescript 4 版本支持了这种语法：

```typescript
type Strings = [string, string];
type Numbers = number[];

// [string, string, ...Array<number | boolean>]
type Unbounded = [...Strings, ...Numbers, boolean];
```

对于再复杂一些的场景，例如高阶函数 `partialCall`，支持一定程度的柯里化：

```typescript
function partialCall(f, ...headArgs) {
  return (...tailArgs) => f(...headArgs, ...tailArgs);
}
```

我们可以通过上面的特性对其进行类型定义，将函数 `f` 第一个参数类型定义为有顺序的 `[...T, ...U]`：

```typescript
type Arr = readonly unknown[];

function partialCall<T extends Arr, U extends Arr, R>(
  f: (...args: [...T, ...U]) => R,
  ...headArgs: T
) {
  return (...b: U) => f(...headArgs, ...b);
}
```

测试效果如下：

```typescript
const foo = (x: string, y: number, z: boolean) => {};

// This doesn't work because we're feeding in the wrong type for 'x'.
const f1 = partialCall(foo, 100);
//                          ~~~
// error! Argument of type 'number' is not assignable to parameter of type 'string'.

// This doesn't work because we're passing in too many arguments.
const f2 = partialCall(foo, "hello", 100, true, "oops");
//                                              ~~~~~~
// error! Expected 4 arguments, but got 5.

// This works! It has the type '(y: number, z: boolean) => void'
const f3 = partialCall(foo, "hello");

// What can we do with f3 now?

f3(123, true); // works!

f3();
// error! Expected 2 arguments, but got 0.

f3(123, "hello");
//      ~~~~~~~
// error! Argument of type '"hello"' is not assignable to parameter of type 'boolean'
```

值得注意的是，`const f3 = partialCall(foo, "hello");` 这段代码由于还没有执行到 `foo`，因此只匹配了第一个 `x:string` 类型，虽然后面 `y: number, z: boolean` 也是必选，但因为 `foo` 函数还未执行，此时只是参数收集阶段，因此不会报错，等到 `f3(123, true)` 执行时就会校验必选参数了，因此 `f3()` 时才会提示参数数量不正确。

### 元组标记

下面两个函数定义在功能上是一样的：

```typescript
function foo(...args: [string, number]): void {
  // ...
}

function foo(arg0: string, arg1: number): void {
  // ...
}
```

但还是有微妙的区别，下面的函数对每个参数都有名称标记，但上面通过解构定义的类型则没有，针对这种情况，Typescript 4 支持了元组标记：

```typescript
type Range = [start: number, end: number];
```

同时也支持与解构一起使用：

```typescript
type Foo = [first: number, second?: string, ...rest: any[]];
```

### Class 从构造函数推断成员变量类型

构造函数在类实例化时负责一些初始化工作，比如为成员变量赋值，在 Typescript 4，在构造函数里对成员变量的赋值可以直接为成员变量推导类型：

```typescript
class Square {
  // Previously: implicit any!
  // Now: inferred to `number`!
  area;
  sideLength;

  constructor(sideLength: number) {
    this.sideLength = sideLength;
    this.area = sideLength ** 2;
  }
}
```

如果对成员变量赋值包含在条件语句中，还能识别出存在 `undefined` 的风险：

```typescript
class Square {
  sideLength;

  constructor(sideLength: number) {
    if (Math.random()) {
      this.sideLength = sideLength;
    }
  }

  get area() {
    return this.sideLength ** 2;
    //     ~~~~~~~~~~~~~~~
    // error! Object is possibly 'undefined'.
  }
}
```

如果在其他函数中初始化，则 TS 不能自动识别，需要用 `!:` 显式申明类型：

```typescript
class Square {
  // definite assignment assertion
  //        v
  sideLength!: number;
  //         ^^^^^^^^
  // type annotation

  constructor(sideLength: number) {
    this.initialize(sideLength);
  }

  initialize(sideLength: number) {
    this.sideLength = sideLength;
  }

  get area() {
    return this.sideLength ** 2;
  }
}
```

### 短路赋值语法

针对以下三种短路语法提供了快捷赋值语法：

```typescript
a &&= b; // a && (a = b)
a ||= b; // a || (a = b)
a ??= b; // a ?? (a = b)
```

### catch error unknown 类型

Typescript 4.0 之后，我们可以将 catch error 定义为 `unknown` 类型，以保证后面的代码以健壮的类型判断方式书写：

```typescript
try {
  // ...
} catch (e) {
  // error!
  // Property 'toUpperCase' does not exist on type 'unknown'.
  console.log(e.toUpperCase());

  if (typeof e === "string") {
    // works!
    // We've narrowed 'e' down to the type 'string'.
    console.log(e.toUpperCase());
  }
}
```

PS：在之前的版本，`catch (e: unknown)` 会报错，提示无法为 `error` 定义 `unknown` 类型。

### 自定义 JSX 工厂

TS 4 支持了 `jsxFragmentFactory` 参数定义 Fragment 工厂函数：

```json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "jsx": "react",
    "jsxFactory": "h",
    "jsxFragmentFactory": "Fragment"
  }
}
```

还可以通过注释方式覆盖单文件的配置：

```typescript
// Note: these pragma comments need to be written
// with a JSDoc-style multiline syntax to take effect.
/** @jsx h */
/** @jsxFrag Fragment */

import { h, Fragment } from "preact";

let stuff = (
  <>
    <div>Hello</div>
  </>
);
```

以上代码编译后解析结果如下：

```typescript
// Note: these pragma comments need to be written
// with a JSDoc-style multiline syntax to take effect.
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "preact";
let stuff = h(Fragment, null, h("div", null, "Hello"));
```

### 其他升级

其他的升级快速介绍：

**构建速度提升**，提升了 `--incremental` + `--noEmitOnError` 场景的构建速度。

**支持 `--incremental` + `--noEmit` 参数同时生效。**

**支持 `@deprecated` 注释，** 使用此注释时，代码中会使用 ~~删除线~~ 警告调用者。

**局部 TS Server 快速启动功能，** 打开大型项目时，TS Server 要准备很久，Typescript 4 在 VSCode 编译器下做了优化，可以提前对当前打开的单文件进行部分语法响应。

**优化自动导入，** 现在 `package.json` `dependencies` 字段定义的依赖将优先作为自动导入的依据，而不再是遍历 `node_modules` 导入一些非预期的包。

除此之外，还有几个 Break Change：

`lib.d.ts` 类型升级，主要是移除了 `document.origin` 定义。

覆盖父 Class 属性的 getter 或 setter 现在都会提示错误。

通过 `delete` 删除的属性必须是可选的，如果试图用 `delete` 删除一个必选的 key，则会提示错误。

## 3 精读

Typescript 4 最大亮点就是可变元组类型了，但可变元组类型也不能解决所有问题。

拿笔者的场景来说，函数 `useDesigner` 作为自定义 React Hook 与 `useSelector` 结合支持 connect redux 数据流的值，其调用方式是这样的：

```typescript
const nameSelector = (state: any) => ({
  name: state.name as string,
});

const ageSelector = (state: any) => ({
  age: state.age as number,
});

const App = () => {
  const { name, age } = useDesigner(nameSelector, ageSelector);
};
```

`name` 与 `age` 是 Selector 注册的，内部实现方式必然是 `useSelector` + reduce，但类型定义就麻烦了，通过重载可以这么做：

```typescript
import * as React from 'react';
import { useSelector } from 'react-redux';

type Function = (...args: any) => any;

export function useDesigner();
export function useDesigner<T1 extends Function>(
  t1: T1
): ReturnType<T1> ;
export function useDesigner<T1 extends Function, T2 extends Function>(
  t1: T1,
  t2: T2
): ReturnType<T1> & ReturnType<T2> ;
export function useDesigner<
  T1 extends Function,
  T2 extends Function,
  T3 extends Function
>(
  t1: T1,
  t2: T2,
  t3: T3,
  t4: T4,
): ReturnType<T1> &
  ReturnType<T2> &
  ReturnType<T3> &
  ReturnType<T4> &
;
export function useDesigner<
  T1 extends Function,
  T2 extends Function,
  T3 extends Function,
  T4 extends Function
>(
  t1: T1,
  t2: T2,
  t3: T3,
  t4: T4
): ReturnType<T1> &
  ReturnType<T2> &
  ReturnType<T3> &
  ReturnType<T4> &
;
export function useDesigner(...selectors: any[]) {
  return useSelector((state) =>
    selectors.reduce((selected, selector) => {
      return {
        ...selected,
        ...selector(state),
      };
    }, {})
  ) as any;
}
```

可以看到，笔者需要将 `useDesigner` 传入的参数通过函数重载方式一一传入，上面的例子只支持到了三个参数，如果传入了第四个参数则函数定义会失效，因此业界做法一般是定义十几个重载，这样会导致函数定义非常冗长。

但参考 TS4 的例子，我们可以避免类型重载，而通过枚举的方式支持：

```typescript
type Func = (state?: any) => any;
type Arr = readonly Func[];

const useDesigner = <T extends Arr>(
  ...selectors: T
): ReturnType<T[0]> &
  ReturnType<T[1]> &
  ReturnType<T[2]> &
  ReturnType<T[3]> => {
  return useSelector((state) =>
    selectors.reduce((selected, selector) => {
      return {
        ...selected,
        ...selector(state),
      };
    }, {})
  ) as any;
};
```

可以看到，最大的变化是不需要写四遍重载了，但由于场景和 `concat` 不同，这个例子返回值不是简单的 `[...T, ...U]`，而是 `reduce` 的结果，所以目前还只能通过枚举的方式支持。

当然可能存在不用枚举就可以支持无限长度的入参类型解析的方案，因笔者水平有限，暂未想到更好的解法，如果你有更好的解法，欢迎告知笔者。

## 4 总结

Typescript 4 带来了更强类型语法，更智能的类型推导，更快的构建速度以及更合理的开发者工具优化，唯一的几个 Break Change 不会对项目带来实质影响，期待正式版的发布。

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

**如果你想参与讨论，请 [点击这里](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)）
