精读原文是 typescript 2.0-2.9 的文档:html
我发现,许多写了一年以上 Typescript 开发者,对 Typescript 对理解和使用水平都停留在入门阶段。形成这个现象的缘由是,Typescript 知识的积累须要 刻意练习,使用 Typescript 的时间与对它的了解程度几乎没有关系。vue
这篇文章精选了 TS 在 2.0-2.9
版本中最重要的功能,并配合实际案例解读,帮助你快速跟上 TS 的更新节奏。webpack
对于 TS 内部优化的用户无感部分并不会罗列出来,由于这些优化均可在平常使用过程当中感觉到。git
因为 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
等因而函数返回值中的 null
或 undefined
。它们都是子类型,好比类型 number
自带了 null
与 undefined
这两个子类型,是由于任何有类型的值都有多是空(也就是执行期间可能没有值)。
这里涉及到很重要的概念,就是预约义了类型不表明类型必定如预期,就比如函数运行时可能由于 throw Error
而中断。因此 ts 为了处理这种状况,将 null
undefined
设定为了全部类型的子类型,而从 2.0
开始,函数的返回值类型又多了一种子类型 never
。
TS 2.2
支持了 object
类型, 但许多时候咱们总把 object
与 any
类型弄混淆,好比下面的代码:
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] };
也是 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]>
,将对象每一个 key
的 value
类型覆盖。基于这些能力,咱们拓展出一系列上层颇有用的 interface
:
2.8
的条件类型语法,实现递归设置只读。"name" | "age"
就能够生成仅支持这两个 key 的新对象类型。2.8
版本才内置进来,能够认为 Pick 是挑选对象的某些 key,Extract 是挑选 key 中的 key。Record
接口才能完成推导。type Omit<T, K> = Pick<T, Exclude<keyof T, K>>
T
的 null
与 undefined
的可能性。T
返回值的类型,这个类型意义很大。以上类型都内置在 lib.d.ts 中,不须要定义就可直接使用,能够认为是 Typescript 的 utils 工具库。
单独拿 ReturnType
举个例子,体现出其重要性:
Redux 的 Connect 第一个参数是 mapStateToProps
,这些 Props 会自动与 React Props 聚合,咱们能够利用 ReturnType<typeof currentMapStateToProps>
拿到当前 Connect 注入给 Props 的类型,就能够打通 Connect 与 React 组件的类型系统了。
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 { //... }
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
。
从 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; } }
Typescript 2.0-2.9
文档总体读下来,能够看出仍是有较强连贯性的。但咱们可能并不习惯一步步学习新语法,由于新语法须要时间消化、同时要链接到以往语法的上下文才能更好理解,因此本文从功能角度,而非版本角度梳理了 TS 的新特性,比较符合学习习惯。
另外一个感悟是,咱们也许要用追月刊漫画的思惟去学习新语言,特别是 TS 这种正在发展中,而且迭代速度很快的语言。
讨论地址是: 精读《Typescript2.0 - 2.9》 · Issue #85 · dt-fe/weekly
若是你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。