精读《Typescript2.0 - 2.9》

1 引言

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

2.0-2.82.9 草案.

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

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

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

2 精读

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

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

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

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

const config = {
  port: 8000
};

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

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

console.log(config!.port);

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

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 总结

增加了 neverobject类型

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

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

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

这里涉及到很重要的概念,就是预定义了类型不代表类型一定如预期,就好比函数运行时可能因为 throw Error而中断。所以 ts 为了处理这种情况,nullundefined设定为了所有类型的子类型,而从 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的。

增加了修饰类型

和最新 ES 规范的私有成员修饰符 #对应,TS 早在 2.0版本就支持了 readonly修饰符,被它修饰的变量无法被修改。

相比 #readonly在编译期就会触发报错,所以笔者更推荐使用 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。把对象 key 全部设置为只读,或者利用 2.8的条件类型语法,实现递归设置只读。
  • Partial。把对象的 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。排除 Tnullundefined的可能性。
  • ReturnType。获取函数 T返回值的类型,这个类型意义很大。
  • InstanceType。获取一个构造函数类型的实例类型。

以上类型都内置在 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");

在 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

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

原文链接:segmentfault.com

上一篇:快速重置vuex数据 - Vue
下一篇:CSS2-盒模型、背景图片

相关推荐

  • 论一个前端工程师如何快速学习,成长。准备自己的35岁 【-原创精读】

    clipboard.png(https://img.javascriptcn.com/5581ba67edbaa03b1fdaf0af674fd3aa "clipboard.png") 前端工程师...

    9 个月前
  • 精读《高性能 javascript》

    前言 本期我来给大家推荐的书是《高性能JavaScript》,在这本书中我们能够了解 javascript 开发过程中的性能瓶颈,如何提升各方面的性能,包括代码的加载、运行、DOM交互、页面生存周...

    2 年前
  • 精读《重新思考 Redux》

    本周精读内容是 《重新思考 Redux》(https://hackernoon.com/redesigningreduxb2baee8b8a38)。 1 引言 《重新思考 Redux》是 rem...

    2 年前
  • 精读《谈谈 Web Workers》

    1 引言 本周精读的文章是 speedyintroductiontowebworkers(https://auth0.com/blog/speedyintroductiontowebworkers...

    2 年前
  • 精读《用 css grid 重新思考布局》

    1 引言 Flex 与 Grid 相比就像功能键盘和触摸屏。触摸屏的控制力相比功能键盘来说就像是降维打击,因为功能键盘只能上下左右控制(x、y 轴),而触摸屏打破了布局障碍,直接从(z 轴)触达,...

    8 个月前
  • 精读《用 Babel 创造自定义 JS 语法》

    1 引言 在写这次精读之前,我想谈谈前端精读可以为读者带来哪些价值,以及如何评判这些价值。 前端精读已经写到第 123 篇了,大家已经不必担心它突然停止更新,因为我已养成每周写一篇文章的习惯,而...

    8 个月前
  • 精读《现代 js 框架存在的根本原因》

    1 引言 深入思考为何前端需要框架,以及 web components 是否可以代替前端框架? 原文地址(https://medium.com/dailyjs/thedeepestreasonw...

    2 年前
  • 精读《深入浅出Node.js》

    (前言 "前言")前言 如果你想要深入学习Node,那你不能错过《深入浅出Node.js》这本书,它从不同的视角介绍了 Node 内在的特点和结构。由首章Node 介绍为索引,涉及Node 的各个...

    2 年前
  • 精读《正则 ES2018》

    1. 引言 本周精读的文章是 regexpfeaturesregularexpressions(https://www.smashingmagazine.com/2019/02/regexpfea...

    1 年前
  • 精读《智能商业》

    1. 引言 智能商业(https://book.douban.com/subject/30357931/) 是阿里巴巴前总参谋长曾鸣于 201811 出版的商业图书,对最近 20 年中国商业以及互...

    1 年前

官方社区

扫码加入 JavaScript 社区