精读《Typescript2.0 - 2.9》

1 引言

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

2.0-2.82.9 草案.前端

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

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

对于 TS 内部优化的用户无感部分并不会罗列出来,由于这些优化均可在平常使用过程当中感觉到。git

2 精读

因为 Typescript 在严格模式下的许多表现都与非严格模式不一样,为了不没必要要的记忆,建议只记严格模式就行了!github

严格模式致使的大量边界检测代码,已经有解了

直接访问一个变量的属性时,若是这个变量是 undefined,不但属性访问不到,js 还会抛出异常,这几乎是业务开发中最高频的报错了(每每是后端数据异常致使的),而 typescript 的 strict 模式会检查这种状况,不容许不安全的代码出现。web

2.0 版本,提供了 “非空断言标志符” !. 解决明确不会报错的状况,好比配置文件是静态的,那确定不会抛出异常,但在 2.0 以前的版本,咱们可能要这么调用对象:typescript

const config = {
  port: 8000
};

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

有了 2.0 提供的 “非空断言标志符”,咱们能够这么写了:npm

console.log(config!.port);

2.8 版本,ts 支持了条件类型语法:json

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

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

这这时能够构造一个自动 “非空断言” 的类型,把代码简化为:

console.log(config.port);

前提是框架先把 config 指定为这个特殊类型,这个特殊类型的定义以下:

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

也就是 2.8 的条件类型容许咱们在类型判断进行递归,把全部对象的 key 都包一层 “非空断言”!

此处灵感来自 egg-ts 总结

增长了 never object 类型

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

好比 throw Error 或者 while(true) 都会致使函数返回值类型时 never

null undefined 特性同样,never 等因而函数返回值中的 nullundefined它们都是子类型,好比类型 number 自带了 nullundefined 这两个子类型,是由于任何有类型的值都有多是空(也就是执行期间可能没有值)。

这里涉及到很重要的概念,就是预约义了类型不表明类型必定如预期,就比如函数运行时可能由于 throw Error 而中断。因此 ts 为了处理这种状况,null undefined 设定为了全部类型的子类型,而从 2.0 开始,函数的返回值类型又多了一种子类型 never

TS 2.2 支持了 object 类型, 但许多时候咱们总把 objectany 类型弄混淆,好比下面的代码:

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" 这种非对象类型:

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>,将对象全部可选修饰移除,天然就成为了必选类型:

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

能够定义函数的 this 类型

也是 TS 2.0 版本中,咱们能够定制 this 的类型,这个在 vue 框架中尤其有用:

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

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

引用、寻址支持通配符了

简单来讲,就是模块名能够用 * 表示任何单词了:

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

它的类型能够辐射到:

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

这个特性很强大的一个点是用在拓展模块上,由于包括 tsconfig.json 的模块查找也支持通配符了!举个例子一下就懂:

最近比较火的 umi 框架,它有一个 locale 插件,只要安装了这个插件,就能够从 umi/locale 获取国际化内容:

import { locale } from "umi/locale";

其实它的实现是建立了一个文件,经过 webpack.alias 将引用指了过去。这个作法很是棒,那么如何为它加上类型支持呢?只要这么配置 tsconfig.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 的类型:

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

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

基于 keyof,咱们能够加强对象的类型:

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]>,将对象每一个 keyvalue 类型覆盖。

基于这些能力,咱们拓展出一系列上层颇有用的 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>。排除 Tnullundefined 的可能性。
  • 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 语法的异步迭代支持:

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

这能够对每一步进行异步迭代。注意对比下面的写法:

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

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

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

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

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

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

若是为了执行顺序,能够换成 for..of 的语法,由于数组类型是一种可迭代类型。

泛型默认参数

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

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

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

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

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

能够清晰的看到,createStore 想表现的是对参数个数的重载,若是定义了函数类型重载,TS 会根据函数类型自动判断对应的是哪一个定义。

而在 TS 2.3 版本支持了泛型默认参数,能够某些场景减小函数类型重载的代码量,好比对于下面的代码:

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 之间可能存在的关系,这些均可以用泛型默认参数解决:

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

尤为在 React 使用过程当中,若是用泛型默认值定义了 Component

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

就能够实现如下等价的效果:

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

动态 Import

TS 从 2.4 版本开始支持了动态 Import,同时 Webpack4.0 也支持了这个语法(在 精读《webpack4.0%20 升级指南》 有详细介绍),这个语法就正式能够用于生产环境了:

const zipUtil = await import("./utils/create-zip-file");
准确的说,动态 Import 实现于 webpack 2.1.0-beta.28,最终在 TS 2.4 版本得到了语法支持。

在 TS 2.9 版本开始,支持了 import() 类型定义:

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:

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

笔者在这提醒一句,这个功能在纯前端代码内可能没有用。由于在 TS 中全部 enum 的地方都建议使用 enum 接收,下面给出例子:

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

不只是可读性,enum 对应的数字可能会改变,直接写 75 的作法存在风险。

但若是先后端存在交互,前端是不可能发送 enum 对象的,必需要转化成数字,这时使用字符串做为 value 会更安全:

enum types {
  Folder = "FOLDER"
}

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

数组类型能够明确长度

最典型的是 chart 图,常常是这样的二维数组数据类型:

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

通常咱们会这么描述其数据结构:

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

在 TS 2.7 版本中,咱们能够更精确的描述每一项的类型与数组总长度:

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

自动类型推导

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

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

instanceof:

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 的推导:

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 了:

// 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

若是你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。

相关文章
相关标签/搜索