摘要:比如或者都会导致函数返回值类型时。和特性一样,等于是函数返回值中的或。注意对比下面的写法对于,它的返回值是可迭代的对象,并且每个类型都是或者。首先是不支持方法重载的,是支持的,而类型系统一定程度在对标,当然要支持这个功能。
1 引言
精读原文是 typescript 2.0-2.9 的文档:
2.0-2.8,2.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 extends string ? "string"
当 T 的类型是 string 时,TypeName 的表达式类型为 "string"。
这这时可以构造一个自动 “非空断言” 的类型,把代码简化为:
console.log(config.port);
前提是框架先把 config 指定为这个特殊类型,这个特殊类型的定义如下:
export type PowerPartial= { [U in keyof T]?: T[U] extends object ? PowerPartial : 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
type Required可以定义函数的 this 类型= { [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", ""] } } }
将所有 umi/* 的类型都指向
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= { [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
基于这些能力,我们拓展出一系列上层很有用的 interface:
Readonly
Partial
Pick
Extract
Record
Exclude
Omit
NonNullable
ReturnType
InstanceType
以上类型都内置在 lib.d.ts 中,不需要定义就可直接使用,可以认为是 Typescript 的 utils 工具库。
多带带拿 ReturnType 举个例子,体现出其重要性:
Redux 的 Connect 第一个参数是 mapStateToProps,这些 Props 会自动与 React Props 聚合,我们可以利用 ReturnType
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; declare function create (element: T): Container ; declare function create ( element: T, children: U[] ): Container ;
通过枚举表达了范型默认值,以及 U 与 T 之间可能存在的关系,这些都可以用泛型默认参数解决:
declare function create( element?: T, children?: U ): Container ;
尤其在 React 使用过程中,如果用泛型默认值定义了 Component:
.. Component..
就可以实现以下等价的效果:
class Component extends React.PureComponent动态 Import{ //... } // 等价于 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。
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自动类型推导{ 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
如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/95308.html
摘要:引言发布了几个新特性,主要变化是类型检查更严格,对一些时髦功能拓展了类型支持。精读这次改动意图非常明显,是为了跟上的新语法。基本可以算是对社区的回馈。讨论地址是精读新特性如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。 1 引言 Typescript 3.2 发布了几个新特性,主要变化是类型检查更严格,对 ES6、ES7 一些时髦功能拓展了类型支持。 2 概要 下面挑一...
摘要:引言本周精读的文章是,看看作者是如何解释这个多态性含义的。读完文章才发现,文章标题改为的多态性更妥当,因为整篇文章都在说,而使用场景不局限于。更多讨论讨论地址是精读的多态性如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。 1 引言 本周精读的文章是:surprising-polymorphism-in-react-applications,看看作者是如何解释这个多态性含...
摘要:引言本周精读的文章是,讲了如何利用实现串行执行。总结串行队列一般情况下用的不多,因为串行会阻塞,而用户交互往往是并行的。更多讨论讨论地址是精读用实现串行执行如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。 1 引言 本周精读的文章是 why-using-reduce-to-sequentially-resolve-promises-works,讲了如何利用 reduce...
阅读 2135·2021-10-08 10:15
阅读 1160·2019-08-30 15:52
阅读 495·2019-08-30 12:54
阅读 1510·2019-08-29 15:10
阅读 2668·2019-08-29 12:44
阅读 2986·2019-08-29 12:28
阅读 3325·2019-08-27 10:57
阅读 2185·2019-08-26 12:24