TypeScript 第二天指南

本文首发于个人我的博客react

杂谈

不知道是就我这样,仍是你们也是,最近的内容圈子里关于 TypeScript 的文章满天飞,各类 TypeScript 有多好、多受欢迎,要不就是 TypeScript 的教程、实践。恰恰我在这时候有了写这篇文章的想法,搞得颇有跟风蹭热度的嫌疑。git

做为一名坚持原创的做者,我并不想把市面上随手可得的东西,换一种方式再讲给你们听,这样不只是在浪费你们的时间,也是在浪费我本身的时间。我所理解的为社区作贡献,应该是可以填补当前环境下的一些空白,去作一些真正有意义的事,而不是摆出一副资深的样子,去转发或是创造一些重复的内容。github

今天这篇文章,虽然有跟风的嫌疑,但我向你保证,内容依然是绝对的原创。若有巧合,那么英雄所见略同。typescript

为何是第二天?

上手 TypeScript 并不难,有 JavaScript 基础的同窗,花个一天时间过一遍文档,基本就都清楚了。若是你恰好还有 Java、C# 等后端语言的基础,那么其中关于 OOP 的一些概念相信你必定会以为很是眼熟。后端

若是你刚看完文档就开始准备把 TypeScript 用到项目中去,那么恭喜你,你很快就会遇到各类坑,并且你没法直接从文档中寻找到对应的解决方案。这篇文章的存在,就是但愿可以填补这中间的空白,帮助各位顺利的把 TypeScript 落地到项目中。markdown

这即是标题中「第二天」的由来。若是你尚未看过 TypeScript 的文档,那么这篇文章如今还不适合你,建议先收藏起来,等看完了文档再回来。数据结构

若是你已经准备好了,那咱们开始吧。编辑器

你或许并不须要 TypeScript

每一个人接触 TypeScript 的缘由不一样,有的是被人安利,有的是由于团队在用,有的是由于用了 Angular。但无论由于什么入了这个坑,咱们都须要明白:TypeScript 并不是必须。函数

TypeScript 适合大型项目,小型项目最好仍是继续用 JavaScript。这已是业内的一个共识。工具

TypeScript 能够简单理解为 JavaScript + Types。从工程效率的角度上讲,Types 的部分属于额外的工做量,若是不能给项目带来足够的收益,去平衡掉其引入的成本,那么这项投入就不是很值得。

若是只是官网之类的小型项目,类型不类型的并不重要,不必为了用 TypeScript 而用 TypeScript。但随着项目的规模和复杂度的增长,代码质量、沟通成本等问题开始浮现,而这偏偏是类型系统可以解决的问题。经过类型检测,咱们能够更早的发现潜在的类型错误,进行主动防护,进而提升代码质量;经过类型定义,咱们能够更加直观的描述咱们的数据结构,下降团队做业中的沟通成本。

所以,要不要用 TypeScript,取决于你项目的类型以及规模,不要盲目跟风。

别忘了 jsDoc

不少人对 TypeScript 有一个误解,以为有了静态类型的代码已经足够「自解释」,就不须要 jsDoc 一类的注释了。

静态类型描述的是数据的结构,而注释描述的是数据的做用,二者解决的是不一样的问题,彼此之间并不冲突。

好比下面这段 JavaScript 代码:

function convert (val, config) {
  // some code
}
复制代码

不难看出这是一个转换函数,接收一个待转换的值,以及一个配置对象,但咱们并不知道这个函数用来转换什么,配置对象又有哪些参数。

如今咱们用 TypeScript 来重写一下,补充一些类型定义。

function convert (val: string, config: { x: string, y? :boolean}): string {
  // some code
}
复制代码

如今咱们知道了这是一个用于处理字符串的转换函数,配置对象有两个参数,一个是必选的字符串,一个是可选的布尔值,最后返回的也是一个字符串。但具体到业务中,这个函数用来转换什么样的字符串,咱们仍是不太清楚。

/** * @description 对手机号进行编码,隐藏其中一部分,如:13812345678 -> 138****5678 * @param val 待编码的手机号 * @param config 配置选项 */
function convert (val: string, config: { x: string, y? :boolean}): string {
  // some code
}
复制代码

加上注释以后,一切就都清楚了:这是一个对手机号进行编码,将其中一部分替换成其余字符,以保护用户隐私的函数。

因此你看,TypeScript 并不能彻底替代 jsDoc 的做用,该写的注释仍是得写。

固然对于上面的例子,若是是一个单纯的工具函数,咱们彻底可使用更加直观的命名,好比 encodeMobile (mobile, config),但若是这是某个类中的成员函数,那么可能就不可避免地会出现示例中的写法。总之,你明白个人意思就好,就不要钻牛角尖了。

对了,得益于 TypeScript 的类型系统,@param 不须要再指定数据类型了,只要对变量的用途进行描述就行了。若是你配置了 Lint 工具,它也会提醒你优先使用 TypeScript 来定义类型,不要重复定义。

TSX 和 JSX

以前咱们在用 JavaScript 写 React 时,对文件的扩展名没有什么特别的要求,*.js 或者 *.jsx 都行。

但在 TypeScript 中,若是你要使用 JSX 语法,就不能使用 *.ts,必须使用 *.tsx。若是你不知道,或者忘了这么作,那么你会在使用了 JSX 代码的地方收到类型报错,但代码自己怎么看都没有问题。这也是刚上手 TypeScript + React 时几乎每一个人都会遇到的坑。

关于这一点,TypeScript 只是在官方教程的示例代码中直接用了 *.tsx,但并无明确说明这一问题。

React 则在它的官方文档中说明了这一规则:

In React, you most likely write your components in a .js file. In TypeScript we have 2 file extensions:

.ts is the default file extension while .tsx is a special extension used for files which contain JSX.

在使用 React 时,咱们一般会把组件写在一个 .*js 文件里。在 TypeScript 中咱们有两种文件扩展名:

.ts 是默认的文件扩展名,而 .tsx 是用于包含了 JSX 的文件的特殊扩展名。

其实上面这段话也没有明说在 *.ts 中使用 JSX 会报错,因此即使有人看到了这段话,可能也觉得只是像 *.jsx 同样多了一种选择,并无太当回事……直到遇到问题。

变量的 Type 怎么找

上手 TypeScript 以后很快咱们就发现,即使是原生的 DOM、或是 React 的 API,也常常会要咱们手动指定类型。但这些结构并非简单的 JavaScript 原始类型,在使用 JavaScript 编写相关代码时候因为没有这种须要,咱们也没关心过这些东西的类型,忽然问起来,还真不知道这些类型叫什么名字。

不光是这些标准类型,一样的问题在不少第三方的库中也会遇到,好比一些组件库会检查你传入的 Props。

在我看来,这中间其实缺乏了一部分的文档,来指导新用户如何找到所须要的类型。既然社区没有提供,那就我来吧。

固然,让每一个开发者都熟记全部的类型确定是不现实的,总不能每接触一个新的库,就要去记一堆类型吧。放心,世界仍是美好的,这种事情,固然是有方法的。

最直白的方法就是去看库的 Types Definition,也就是那些 .*d.ts 文件。若是你恰好有在用 VS Code 的话,有一个很是方便的操做:把鼠标移动到你想知道它类型的代码上(好比某个变量、某个函数调用,或是某个 JSX 标签、某个组件的 props),右键选择「Go to Definition」(或者光标选中后按 F12),就能够跳转到它的类型定义文件了。

若是你更习惯使用 VS Code 以外的编辑器,我相信时至今日,它们应该也都早就对 TypeScript 提供了支持。具体操做我不太熟悉,你能够本身探索下(我一直用 VS Code,其它的不太熟)。

通常来讲,这个操做能够直接把你带到你想要的地方,但考虑到类型是能够继承的,有时候一次跳转可能不太够,遇到这种状况,那就须要你随机应变一下,沿着继承关系多跳几回,直到找到你想要的内容。

对于不熟悉的类型,能够经过这个方法去寻找,慢慢熟悉之后,你会发现,一些常见的类型仍是很好找的,稍微联想一下英文的表达方式,配合自动补全的提示,通常都不难找到。

为了方便初学者,咱们仍是稍微列举一些常见的类型,找找感受:

常见 Types 之 DOM

TypeScript 自带了一些基本的类型定义,包括 ECMAScript 和 DOM 的类型定义,全部你须要的类型均可以从这里找到。若是你想作一些「纯 TypeScript 开发」的话,有这些就够了。

好比下面这张截图,就是对 <div> 标签的类型定义。咱们能够看到,它继承了更加通用的 HTMLElement 类型,而且扩展了一个即将被废弃的 align 属性,以及两组 addEventListenerremoveEventListener,注意这里使用了重载。

这里的命名也不是随便起的,都是在 MDN 上能够查到的。

仍是以 <div> 为例,咱们已经知道它继承自 HTMLElement,其实再往上,HTMLElement 继承自 ElementElement 又继承自 Node,顺着这条路,你能够挖掘出全部 HTML 标签的类型。

对于一些 DOM 相关的属性,好比 onclickonchange 等,你均可以如法炮制,找到它们的定义。

常见 Types 之 React

关于 TypeScript 的问题,有很多实际上是在使用第三方库的时候遇到的,React 就是其中比较典型的一个。

其实方法都同样,只不过相关的类型定义不在 TypeScript 中,而是在 @types/react 中。

React 的类型定义的名称其实也很直观,好比咱们常见的 React.Component,在定义 Class 组件时,咱们须要对 Props 和 State 预先进行类型定义,为何呢?答案就在它的类型定义中。

再好比,当咱们在写一些组件时,咱们可能会须要向下传递 this.props.children,但 children 并无被设为默认值,须要咱们本身定义到 props 上,那么它的类型应该是什么呢?

到类型定义中搜一下关键字 children,很快咱们就找到了下面的定义:

全部 React 中 JSX 所表明的内容,不管是 render() 的返回,仍是 children,咱们均可以定义为一个 ReactNode。那这个 ReactNode 长什么样呢?咱们经过右键继续寻找:

看到这里,咱们不光找到了咱们想要的类型,还顺带明白了为何 render() 能够返回 boolean、null、undefined 表示不渲染任何内容。

那么事件呢?当咱们给组件定义事件处理函数的时候,也常常会被要求指定类型。仍是老办法,找不到咱就搜,好比 onClick 不清楚,那咱们就以它为关键字去搜:

据此咱们找到一个叫 MouseEventHandler 的定义,这名字,够直白吧。

好了,咱们找到想要的了。不过既然来了,不如继续看一下,看看还能发现什么。咱们右键 MouseEventHandler 急需往下看:

看到了吗,全部的事件处理函数都有对应的定义,每一个都须要一个泛型参数,传递了事件的类型,名称也挺直白的。

Ok,事件的类型也被咱们挖出来了,之后若是须要单独定义一个事件相关的类型,就能够直接用了。

以此类推,无论是什么东西的类型,均可以去它们对应的 @types/xxx 里,按关键字搜,只要你的英语别太差,很容易就能找到。

多重 extends

咱们知道 Interface 是能够多继承的,extends 后面能够跟多个其它 Interface,咱们不能保证被继承的多个 Interface 必定没有重复的属性,那么当属性重复,但类型定义不一样时,最终的结果会怎么样呢?

在 TypeScript 中,Interface 会按照从右往左的顺序去合并多个被继承的 Interface,也就是说,同名属性,左边的会覆盖右边的。

interface A {
  value?: string
}

interface B {
  value: string
}

interface C {
  value: number
}

interface D extends A, B {}
// value?: string

interface E extends B, C {}
// value: string
复制代码

obj[prop] 没法访问怎么办

有时候咱们会定义一些集合型的数据,例如对象、枚举等,但在调用的时候,咱们未必会直接经过 obj.prop 的形式去调用,可能会是以 obj[prop] 这种动态索引的形式去访问,但经过动态索引的方式就没法肯定最终访问的元素是否存在,所以在 TypeScript 中,默认是不容许这种操做的。

但这又是个很是合理,并且很是常见的场景,怎么办呢?TypeScript 容许为类型添加索引,以实现这一点。

interface Foo {
  x: string,
  y: number
  [index: string]: string | number
}
复制代码

这个方法虽然有效,但每次都要手动为类型加索引,重复多了也挺心累的。包括在一些「配置对象」中,咱们甚至没法肯定有哪些类型,有没有一种更加通用、更加一劳永逸的方法。

固然有。

其实在 TypeScript 的官方文档中就有提到这个方案,官方管它叫 OptionBag,大概就是指 config、option 等用于提供配置信息的这么一类参数。我不是很肯定这究竟是个常规的英文单词,仍是 TypeScript 中特定的术语(我的感受是前者),反正就这么个意思吧。

简单说来,咱们能够定义下面这样一个类型:

interface OptionBag {
  [index: string]: any
}
复制代码

这是一个很是通用的结构,以字符串为键,值能够是任何类型,而且支持索引 —— 这不就是 Object 么。

以后全部须要动态索引的结构,或是做为配置对象的结构,均可以直接指定为,或是继承 OptionBag。这个方案以牺牲必定的类型检查为代价,换取了操做上的便利。

理论上讲,OptionBag 能够适用于全部相似对象这样的结构,但不建议各位真就这么作。这个方案只能是用在一些对类型要求不那么严格,或是没法预知类型的场景中,可以肯定的类型仍是尽量地写一下,不然就失去了使用 TypeScript 意义了。

小结

TypeScript 确实是个好东西,但世上没有绝对完美的东西,实践过程当中总会有那么些阻碍完咱们前进的坑。可是掉坑里并不可怕,只要有办法能爬出来,那就都不叫事儿。

原创不易,坚持原创更是,但愿这篇文章多少能给你们带来一些收获吧。

相关文章
相关标签/搜索