## 1 引言

精读原文是 typescript 2.0-2.9 的文档:

[2.0-2.8](http://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html)，[2.9 草案](https://blogs.msdn.microsoft.com/typescript/2018/05/16/announcing-typescript-2-9-rc/).

我发现，许多写了一年以上 Typescript 开发者，对 Typescript 对理解和使用水平都停留在入门阶段。造成这个现象的原因是，Typescript 知识的积累需要 **刻意练习**，使用 Typescript 的时间与对它的了解程度几乎没有关系。

这篇文章精选了 TS 在 `2.0-2.9` 版本中最重要的功能，并配合实际案例解读，帮助你快速跟上 TS 的更新节奏。

对于 TS 内部优化的用户无感部分并不会罗列出来，因为这些优化都可在日常使用过程中感受到。

## 2 精读

由于 Typescript 在严格模式下的许多表现都与非严格模式不同，为了避免不必要的记忆，建议只记严格模式就好了！

### 严格模式导致的大量边界检测代码，已经有解了

直接访问一个变量的属性时，如果这个变量是 `undefined`，不但属性访问不到，js 还会抛出异常，这几乎是业务开发中最高频的报错了（往往是后端数据异常导致的），而 typescript 的 `strict` 模式会检查这种情况，不允许不安全的代码出现。

在 `2.0` 版本，提供了 “非空断言标志符” `!.` 解决明确不会报错的情况，比如配置文件是静态的，那肯定不会抛出异常，但在 `2.0` 之前的版本，我们可能要这么调用对象：

```typescript
const config = {
  port: 8000
};

if (config) {
  console.log(config.port);
}
```

有了 `2.0` 提供的 “非空断言标志符”，我们可以这么写了：

```typescript
console.log(config!.port);
```

在 `2.8` 版本，ts 支持了条件类型语法：

```typescript
type TypeName<T> = T extends string ? "string"
```

当 T 的类型是 string 时，TypeName 的表达式类型为 "string"。

这这时可以构造一个自动 “非空断言” 的类型，把代码简化为：

```typescript
console.log(config.port);
```

前提是框架先把 `config` 指定为这个特殊类型，这个特殊类型的定义如下：

```typescript
export type PowerPartial<T> = {
  [U in keyof T]?: T[U] extends object ? PowerPartial<T[U]> : T[U]
};
```

也就是 `2.8` 的条件类型允许我们在类型判断进行递归，把所有对象的 key 都包一层 “非空断言”！

> 此处灵感来自 [egg-ts 总结](https://github.com/whxaxes/blog/issues/12)

### 增加了 `never` `object` 类型

当一个函数无法执行完，或者理解为中途中断时，TS `2.0` 认为它是 `never` 类型。

比如 `throw Error` 或者 `while(true)` 都会导致函数返回值类型时 `never`。

和 `null` `undefined` 特性一样，`never` 等于是函数返回值中的 `null` 或 `undefined`。**它们都是子类型**，比如类型 `number` 自带了 `null` 与 `undefined` 这两个子类型，是因为任何有类型的值都有可能是空（也就是执行期间可能没有值）。

这里涉及到很重要的概念，就是预定义了类型不代表类型一定如预期，就好比函数运行时可能因为 `throw Error` 而中断。所以 ts 为了处理这种情况，**将 `null` `undefined` 设定为了所有类型的子类型**，而从 `2.0` 开始，函数的返回值类型又多了一种子类型 `never`。

TS `2.2` 支持了 `object` 类型， 但许多时候我们总把 `object` 与 `any` 类型弄混淆，比如下面的代码：

```typescript
const persion: object = {
  age: 5
};
console.log(persion.age); // Error: Property 'age' does not exist on type 'object'.
```

这时候报错会出现，有时候闭个眼改成 `any` 就完事了。其实这时候只要把 `object` 删掉，换成 TS 的自动推导就搞定了。那么问题出在哪里？

首先 `object` 不是这么用的，它是 TS `2.3` 版本中加入的，用来描述一种非基础类型，所以一般用在类型校验上，比如作为参数类型。如果参数类型是 `object`，那么允许任何对象数据传入，但不允许 `3` `"abc"` 这种非对象类型：

```typescript
declare function create(o: object | null): void;

create({ prop: 0 }); // 正确
create(null); // 正确

create(42); // 错误
create("string"); // 错误
create(false); // 错误
create(undefined); // 错误
```

而一开始 `const persion: object` 这种用法，是将能精确推导的对象类型，扩大到了整体的，模糊的对象类型，TS 自然无法推断这个对象拥有哪些 `key`，因为对象类型仅表示它是一个对象类型，在将对象作为整体观察时是成立的，但是 `object` 类型是不承认任何具体的 `key` 的。

### 增加了修饰类型

TS 在 `2.0` 版本支持了 `readonly` 修饰符，被它修饰的变量无法被修改。

在 TS `2.8` 版本，又增加了 `-` 与 `+` 修饰修饰符，有点像副词作用于形容词。举个例子，`readonly` 就是 `+readonly`，我们也可以使用 `-readonly` 移除只读的特性；也可以通过 `-?:` 的方式移除可选类型，因此可以延伸出一种新类型：`Required<T>`，将对象所有可选修饰移除，自然就成为了必选类型：

```typescript
type Required<T> = { [P in keyof T]-?: T[P] };
```

### 可以定义函数的 this 类型

也是 TS `2.0` 版本中，我们可以定制 `this` 的类型，这个在 `vue` 框架中尤为有用：

```typescript
function f(this: void) {
  // make sure `this` is unusable in this standalone function
}
```

`this` 类型是一种假参数，所以并不会影响函数真正参数数量与位置，只不过它定义在参数位置上，而且永远会插队在第一个。

### 引用、寻址支持通配符了

简单来说，就是模块名可以用 `*` 表示任何单词了：

```typescript
declare module "*!text" {
  const content: string;
  export default content;
}
```

它的类型可以辐射到：

```typescript
import fileContent from "./xyz.txt!text";
```

这个特性很强大的一个点是用在拓展模块上，因为包括 `tsconfig.json` 的模块查找也支持通配符了！举个例子一下就懂：

最近比较火的 `umi` 框架，它有一个 `locale` 插件，只要安装了这个插件，就可以从 `umi/locale` 获取国际化内容：

```typescript
import { locale } from "umi/locale";
```

其实它的实现是创建了一个文件，通过 `webpack.alias` 将引用指了过去。这个做法非常棒，那么如何为它加上类型支持呢？只要这么配置 `tsconfig.json`:

```json
{
  "compilerOptions": {
    "paths": {
      "umi/*": ["umi", "<somePath>"]
    }
  }
}
```

将所有 `umi/*` 的类型都指向 `<somePath>`，那么 `umi/locale` 就会指向 `<somePath>/locale.ts` 这个文件，如果插件自动创建的文件名也恰好叫 `locale.ts`，那么类型就自动对应上了。

### 跳过仓库类型报错

TS 在 `2.x` 支持了许多新 `compileOptions`，但 `skipLibCheck` 实在是太耀眼了，笔者必须单独提出来说。

`skipLibCheck` 这个属性不但可以忽略 npm 不规范带来的报错，还能最大限度的支持类型系统，可谓一举两得。

拿某 UI 库举例，某天发布的小版本 `d.ts` 文件出现一个漏洞，导致整个项目构建失败，你不再需要提 PR 催促作者修复了！`skipLibCheck` 可以忽略这种报错，同时还能保持类型的自动推导，也就是说这比 `declare module "ui-lib"` 将类型设置为 `any` 更强大。

### 对类型修饰的增强

TS `2.1` 版本可谓是针对类型操作革命性的版本，我们可以通过 `keyof` 拿到对象 key 的类型：

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

type K1 = keyof Person; // "name" | "age"
```

基于 `keyof`，我们可以增强对象的类型：

```typescript
type NewObjType<T> = { [P in keyof T]: T[P] };
```

Tips：在 TS `2.8` 版本，我们可以以表达式作为 `keyof` 的参数，比如 `keyof (A & B)`。
Tips：在 TS `2.9` 版本，`keyof` 可能返回非 `string` 类型的值，因此从一开始就不要认为 `keyof` 的返回类型一定是 `string`。

`NewObjType` 原封不动的将对象类型重新描述了一遍，这看上去没什么意义。但实际上我们有三处拓展的地方：

* 左边：比如可以通过 `readonly` 修饰，将对象的属性变成只读。
* 中间：比如将 `:` 改成 `?:`，将对象所有属性变成可选。
* 右边：比如套一层 `Promise<T[P]>`，将对象每个 `key` 的 `value` 类型覆盖。

基于这些能力，我们拓展出一系列上层很有用的 `interface`：

* Readonly<T>。把对象 key 全部设置为只读，或者利用 `2.8` 的条件类型语法，实现递归设置只读。
* Partial<T>。把对象的 key 都设置为可选。
* Pick<T, K>。从对象类型 T 挑选一些属性 K，比如对象拥有 10 个 key，只需要将 K 设置为 `"name" | "age"` 就可以生成仅支持这两个 key 的新对象类型。
* Extract<T, U>。是 Pick 的底层 API，直到 `2.8` 版本才内置进来，可以认为 Pick 是挑选对象的某些 key，Extract 是挑选 key 中的 key。
* Record<K, U>。将对象某些属性转换成另一个类型。比较常见用在回调场景，回调函数返回的类型会覆盖对象每一个 key 的类型，此时类型系统需要 `Record` 接口才能完成推导。
* Exclude<T, U>。将 T 中的 U 类型排除，和 Extract 功能相反。
* Omit<T, K>（未内置）。从对象 T 中排除 key 是 K 的属性。可以利用内置类型方便推导出来：`type Omit<T, K> = Pick<T, Exclude<keyof T, K>>`
* NonNullable<T>。排除 `T` 的 `null` 与 `undefined` 的可能性。
* ReturnType<T>。获取函数 `T` 返回值的类型，这个类型意义很大。
* InstanceType<T>。获取一个构造函数类型的实例类型。

> 以上类型都内置在 lib.d.ts 中，不需要定义就可直接使用，可以认为是 Typescript 的 utils 工具库。

单独拿 `ReturnType` 举个例子，体现出其重要性：

Redux 的 Connect 第一个参数是 `mapStateToProps`，这些 Props 会自动与 React Props 聚合，我们可以利用 `ReturnType<typeof currentMapStateToProps>` 拿到当前 Connect 注入给 Props 的类型，就可以打通 Connect 与 React 组件的类型系统了。

### 对 Generators 和 async/await 的类型定义

TS `2.3` 版本做了许多对 Generators 的增强，但实际上我们早已用 async/await 替代了它，所以 TS 对 Generators 的增强可以忽略。需要注意的一块是对 `for..of` 语法的异步迭代支持：

```typescript
async function f() {
  for await (const x of fn1()) {
    console.log(x);
  }
}
```

这可以对每一步进行异步迭代。注意对比下面的写法：

```typescript
async function f() {
  for (const x of await fn2()) {
    console.log(x);
  }
}
```

对于 `fn1`，它的返回值是可迭代的对象，并且每个 item 类型都是 Promise 或者 Generator。对于 `fn2`，它自身是个异步函数，返回值是可迭代的，而且每个 item 都不是异步的。举个例子：

```typescript
function fn1() {
  return [Promise.resolve(1), Promise.resolve(2)];
}

function fn2() {
  return [1, 2];
}
```

在这里顺带一提，对 `Array.map` 的每一项进行异步等待的方法：

```typescript
await Promise.all(
  arr.map(async item => {
    return await item.run();
  })
);
```

如果为了执行顺序，可以换成 `for..of` 的语法，因为数组类型是一种可迭代类型。

### 泛型默认参数

了解这个之前，先介绍一下 TS `2.0` 之前就支持的函数类型重载。

首先 JS 是不支持方法重载的，Java 是支持的，而 TS 类型系统一定程度在对标 Java，当然要支持这个功能。好在 JS 有一些偏方实现伪方法重载，典型的是 redux 的 `createStore`：

```typescript
export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === "function" && typeof enhancer === "undefined") {
    enhancer = preloadedState;
    preloadedState = undefined;
  }
}
```

既然 JS 有办法支持方法重载，那 TS 补充了函数类型重载，两者结合就等于 Java 方法重载：

```typescript
declare function createStore(
  reducer: Reducer,
  preloadedState: PreloadedState,
  enhancer: Enhancer
);
declare function createStore(reducer: Reducer, enhancer: Enhancer);
```

可以清晰的看到，`createStore` 想表现的是对参数个数的重载，如果定义了函数类型重载，TS 会根据函数类型自动判断对应的是哪个定义。

而在 TS `2.3` 版本支持了泛型默认参数，**可以某些场景减少函数类型重载的代码量**，比如对于下面的代码：

```typescript
declare function create(): Container<HTMLDivElement, HTMLDivElement[]>;
declare function create<T extends HTMLElement>(element: T): Container<T, T[]>;
declare function create<T extends HTMLElement, U extends HTMLElement>(
  element: T,
  children: U[]
): Container<T, U[]>;
```

通过枚举表达了范型默认值，以及 U 与 T 之间可能存在的关系，这些都可以用泛型默认参数解决：

```typescript
declare function create<T extends HTMLElement = HTMLDivElement, U = T[]>(
  element?: T,
  children?: U
): Container<T, U>;
```

尤其在 React 使用过程中，如果用泛型默认值定义了 `Component`：

```typescript
.. Component<Props = {}, State = {}> ..
```

就可以实现以下等价的效果：

```typescript
class Component extends React.PureComponent<any, any> {
  //...
}
// 等价于
class Component extends React.PureComponent {
  //...
}
```

### 动态 Import

TS 从 `2.4` 版本开始支持了动态 Import，同时 Webpack4.0 也支持了这个语法（在 [精读《webpack4.0%20 升级指南》](https://github.com/dt-fe/weekly/blob/master/47.%E7%B2%BE%E8%AF%BB%E3%80%8Awebpack4.0%20%E5%8D%87%E7%BA%A7%E6%8C%87%E5%8D%97%E3%80%8B.md) 有详细介绍），这个语法就正式可以用于生产环境了：

```typescript
const zipUtil = await import("./utils/create-zip-file");
```

> 准确的说，动态 Import 实现于 webpack 2.1.0-beta.28，最终在 TS `2.4` 版本获得了语法支持。

在 TS `2.9` 版本开始，支持了 `import()` 类型定义：

```typescript
const zipUtil: typeof import('./utils/create-zip-file') = await import('./utils/create-zip-file')
```

也就是 `typeof` 可以作用于 `import()` 语法，而不真正引入 js 内容。不过要注意的是，这个 `import('./utils/create-zip-file')` 路径需要可被推导，比如要存在这个 npm 模块、相对路径、或者在 `tsconfig.json` 定义了 `paths`。

好在 `import` 语法本身限制了路径必须是字面量，使得自动推导的成功率非常高，只要是正确的代码几乎一定可以推导出来。好吧，所以这也从另一个角度推荐大家放弃 `require`。

### Enum 类型支持字符串

从 Typescript `2.4` 开始，支持了枚举类型使用字符串做为 value：

```typescript
enum Colors {
  Red = "RED",
  Green = "GREEN",
  Blue = "BLUE"
}
```

笔者在这提醒一句，这个功能在纯前端代码内可能没有用。因为在 TS 中所有 `enum` 的地方都建议使用 `enum` 接收，下面给出例子：

```typescript
// 正确
{
  type: monaco.languages.types.Folder;
}
// 错误
{
  type: 75;
}
```

不仅是可读性，`enum` 对应的数字可能会改变，直接写 `75` 的做法存在风险。

但如果前后端存在交互，前端是不可能发送 `enum` 对象的，必须要转化成数字，这时使用字符串作为 value 会更安全：

```typescript
enum types {
  Folder = "FOLDER"
}

fetch(`/api?type=${monaco.languages.types.Folder}`);
```

### 数组类型可以明确长度

最典型的是 chart 图，经常是这样的二维数组数据类型：

```json
[[1, 5.5], [2, 3.7], [3, 2.0], [4, 5.9], [5, 3.9]]
```

一般我们会这么描述其数据结构：

```typescript
const data: number[][] = [[1, 5.5], [2, 3.7], [3, 2.0], [4, 5.9], [5, 3.9]];
```

在 TS `2.7` 版本中，我们可以更精确的描述每一项的类型与数组总长度：

```typescript
interface ChartData extends Array<number> {
  0: number;
  1: number;
  length: 2;
}
```

### 自动类型推导

自动类型推导有两种，分别是 `typeof`:

```typescript
function foo(x: string | number) {
  if (typeof x === "string") {
    return x; // string
  }
  return x; // number
}
```

和 `instanceof`:

```typescript
function f1(x: B | C | D) {
  if (x instanceof B) {
    x; // B
  } else if (x instanceof C) {
    x; // C
  } else {
    x; // D
  }
}
```

在 TS `2.7` 版本中，新增了 `in` 的推导：

```typescript
interface A {
  a: number;
}
interface B {
  b: string;
}

function foo(x: A | B) {
  if ("a" in x) {
    return x.a;
  }
  return x.b;
}
```

这个解决了 `object` 类型的自动推导问题，因为 `object` 既无法用 `keyof` 也无法用 `instanceof` 判定类型，因此找到对象的特征吧，再也不要用 `as` 了：

```typescript
// Bad
function foo(x: A | B) {
  // I know it's A, but i can't describe it.
  (x as A).keyofA;
}

// Good
function foo(x: A | B) {
  // I know it's A, because it has property `keyofA`
  if ("keyofA" in x) {
    x.keyofA;
  }
}
```

## 4 总结

Typescript `2.0-2.9` 文档整体读下来，可以看出还是有较强连贯性的。但我们可能并不习惯一步步学习新语法，因为新语法需要时间消化、同时要连接到以往语法的上下文才能更好理解，所以本文从功能角度，而非版本角度梳理了 TS 的新特性，比较符合学习习惯。

另一个感悟是，我们也许要用追月刊漫画的思维去学习新语言，特别是 TS 这种正在发展中，并且迭代速度很快的语言。

## 5 更多讨论

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

**如果你想参与讨论，请[点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。**
