你好,我是大圣。
在讲组件化的进阶开发篇之前,我想在全家桶实战篇的最后,用一讲的篇幅,来专门聊一下 TypeScript。希望你在学完这一讲之后,能对 TypeScript 有一个全面的认识。
另外,今天我会设置很多实战练习,一边阅读一边敲代码的话,学习效果更好。而且,这次加餐中的全部代码都是可以在线完成的,建议你打开这个链接,把下面的每行代码都跟着敲一遍。
TypeScript 入门
对于 TypeScript,你首先要了解的是,TypeScript 可以在 JavaScript 的基础上,对变量的数据类型加以限制。TypeScript 中最基本的数据类型包括布尔、数字、字符串、null、undefined,这些都很好理解。
在下面的代码中,我们分别定义了这几个数据类型的变量,你能看到,当我们把 number 类型的变量 price 赋值字符串时,就会报错,当我们把数组 me 的第一个元素 me[0] 的值修改为数字时,也会报错。
当你不确定某个变量是什么类型时,你可以使用 any 作为这个变量的类型。你可以用 any 标记任何属性,可以修改任何数据,访问任何方法也不会报错。也就是说,在 TypeScript 中,当你把变量的类型标记为 any 后,这个变量的使用就和 JavaScript 没啥区别了,错误只会在浏览器里运行的时候才会提示。
然后我们可以使用 enum 去定义枚举类型,这样可以把类型限制在指定的场景之内。下面的代码中我们可以把课程评分限制在好、非常好和嘎嘎好三个值之内。
然后我们可以通过学到的这些基础类型,通过组合的方式组合出新的类型,最常见的组合方式就是使用 | 实现类型联合。下面的代码中我们定义 course1 变量的类型为字符串或者数字,赋值为这两个类型都不会报错,还可以用来限制变量只能赋值为几个字符串的一个,score 的取值只能是代码中三个值之一。
通过 interface 接口可以定义对象的类型限制。下面代码中我们定义了极客时间课程的类型,课程名是字符串,价格使用 number[] 语法定义类型为数字组成的数组,讲师头像是 string 或者 boolean,并且通过 ? 设置为可选属性,课程地址使用 readonly 设置为只读属性,如果对课程地址进行修改就会报错。
然后我们学一下函数的类型限制。其实函数的定义,参数和返回值本质上也是变量的概念,都可以进行类型的定义。下面的代码中我们定义了参数 x 和 y 是数字,返回值也是数字的 add 函数,定义好参数和返回值类型,函数的类型自然也就确定了。
我们也可以使用变量的方式去定义函数,直接使用 (参数类型) ⇒ 返回值类型的语法去定义 add1 的变量类型,但是这样写出来的代码可读性稍差一些,我更建议你使用 type 或者 interface 关键字去定义函数的类型。下面代码中的 addType 和 addType1 都是很好的定义函数类型的方式:
如果你的函数本来就支持多个类型的参数,下面的代码中 reverse 函数既支持数字也支持字符串。我们的要求是如果参数是数字,返回值也要是数字,参数是字符串返回值也只能是字符串,所以参数和返回值都用 number|string 就没法精确地限制这个需求。我们需要使用函数重载的方式,定义多个函数的输入值和返回值类型,更精确地限制函数的类型。我们可以在Vue 3 的源码看到 Vue 3 中 ref 函数的重载写法:
这样 TypeScript 里如何限制一个变量和函数类型,我们就大致入门了。这时候你肯定还有个疑问,日常开发中有很多浏览器上的变量和属性,这些怎么限制类型呢?
关于宿主环境里的类型,TypeScript 全部都给我们提供了,我们可以直接在代码中书写:Window 是 window 的类型,HTMLElement 是 dom 元素类型,NodeList 是节点列表类型,MouseEvent 是鼠标点击事件的类型……关于更多 TypeScript 的内置类型,你可以在TypeScript 的源码中看到:
除了浏览器的 API,我们还会用到很多第三方框架,比如 Vue、Element3 等等,这些框架现在都提供了完美的类型可以直接使用。在第 18 讲中我们使用下面的代码 Vue 导出的 Ref 来限定数据是 ref 包裹的响应式数据:
泛型
那么聊完上面的内容,你就已经能使用 TypeScript 实现很多项目的开发,把所有变量和函数出现的地方都定义好类型,就可以在编译阶段提前规避出很多报错。然而 TypeScript 的能力可不止于此,TypeScript 可以进行类型编程,这会极大提高 TypeScript 在复杂场景下的应用场景。
然后我们来看一下 TypeScript 中的泛型,这也是很多同学觉得 TypeScript 很难的最大原因。
首先我们看下面的代码,我们定一个 idientity0 函数,这个函数逻辑非常简单,就是直接返回参数,那么我们怎么确定返回值的类型呢?
因为输入值可以是任意属性,所以我们只能写出 identity0 这个函数,参数和返回值类型都是 any,但是明显不能满足我们的需求。我们需要返回值的类型和参数一致,所以我们在函数名之后使用 <> 定一个泛型 T,你可以理解这个 T 的意思就是给函数参数定义了一个类型变量,会在后面使用,相当于【type T = arg 的类型】,返回值使用 T 这个类型就完成了这个需求。
有了泛型之后,我们就有了把函数参数定义成类型的功能,我们就可以实现类似高阶函数的类型函数。下面的代码中我们使用 keyof 语法获得已知类型 VueCourse5 的属性列表,相当于 ‘name’|‘price’:
keyof 可以帮助我们拆解已有类型,下一步我们需要使用 extends 来实现类型系统中的条件判断。我们定义类型函数 ExtendsType,接受泛型参数 T 后,通过判断 T 是不是布尔值来返回不同的类型字符串,我们就可以通过 ExtendsType 传入不同的参数去返回不同的类型。
extends 相当于 TypeScript 世界中的条件语句,然后 in 关键字可以理解为 TypeScript 世界中的遍历。下面的代码中我们通过 k in Courses 语法,相当于遍历了 Courses 所有的类型作为 CourseObj 的属性,值的类型是 number。
学完上面的语法,你就能完全搞懂第 18 讲里的 getProperty 函数。限制函数第二个参数只能是第一个参数的属性,并且返回值的类型,最后我们传递不存在的属性时,TypeScript 就会报错。
然后我再给你讲解最后一个关键字 infer。<T>
让我们拥有了给函数的参数定义类型变量的能力,infer 则是可以在 extends 之后的变量设置类型变量,更加细致地控制类型。下面的代码中我们定义了 ReturnType 类型函数,目的是返回传入函数的返回值类型。infer P 的意思就是泛型 T 是函数类型,并且这个函数类型的返回类型是 P。
实战练习
有了上面的基础后,我们来几个实战的练习。以下所有的练习都可以在代码最后找到答案,我建议你一定要自己实现一遍才能有最多的收获。
代码地址: https://www.typescriptlang.org/docs/handbook/utility-types.html
下面的代码中,我们首先定义类型 Todo,有 title、desc 和 done 三个属性:
首先第一题是,我们需要实现类型函数 Partial1,返回的类型是 Todo 所有的属性都变成可选项。
这一题的答案见下面的代码,使用 K in keyof T 遍历所有 T 的属性后,使用 ? 标记为可选属性。
TypeScript 中还有很多类似的函数,包括 Pick、Omit、Diff 等函数,你都可以自行实现一遍,更多工具类型函数你可以移步TypeScript 官方文档。你也可以结合下面的代码工具函数的实现,到留言区中讨论一下分别实现了什么功能。
最后我们再来一个实战的练习,在实际项目开发中除了 JavaScript、浏览器和第三方框架的类型,还有一个很重要的场景就是后端返回的数据类型。我们需要根据开发文档去定义好每个请求的类型,在下面的代码中,request 作为发送请求的函数,可以传递 url 是字符串。那我们该如何定义 Interface API,使其能够限制 request 只能有 buy 和 comment 两个请求地址,并且 comment 请求的参数中 message 是必传项呢?
记得要先尝试自己实现一下,答案就在下面的代码中。在 API 类型中,我们定义了 buy 和 comment 两个属性,分别设置了当前请求所需要的参数都是必选项。然后我们通过在 request 中使用泛型 T 限制 url,通过 Api[T]
限制传递的参数,这样我们就得到了下面的报错示意图,在编译阶段就能通知你缺少 message 属性,并且 404 请求不存在,这可以极大提高我们开发的体验和效率。
更多类型的练习,你可以访问type-challenges 这个项目自行尝试。现在你再去看项目或者框架源码中的 TypeScript,是不是就没有那么晦涩了呢。
总结
今天想聊的 TypeScript 就结束啦,总结一下我们今天学到的内容吧。
首先我们学习了 TypeScript 的基本类型,包括数字字符串等等,然后我们可以通过这些基础类型组合出复杂的类型组合,并且可以通过 type 和 interface 关键字定义复杂对象的类型和函数的类型。
然后浏览器的相关变量和 API 类型 TypeScript 都已经内置了,包括 HTMLElement、MouseEvent 等等,第三方框架的类型我们可以直接导入使用,Vue 中的 Ref 类型我们就会经常用来定义 ref 函数包裹的响应式数据。
接着我们学习了 TypeScript 进阶中最重要的概念:泛型。通过泛型我们可以在函数内部把类型变成变量使用,并且通过 keyof、in、extends、infer 等关键字组合出复杂的类型函数,可以更加精确地组合现有类型。
最后我们通过定义前后端的接口类型的案例,演示了在实战中我们如何通过类型系统提高联调和开发的体验。
有了这些 TypeScript 的知识储备,你才能更好地在 Vue 项目中使用 TypeScript。由于 JSX 的本质就是 JavaScript,所以 TypeScript 的诞生也给了 JSX 更好的类型推导,这是 JSX 相比于 Template 的另外一个优势。关于 Vue 和 TypeScript 开发组件的内容,我们下一讲开始全部使用 TypeScript 来实现。
思考题
最后,留一个思考题:我们实现的 Partial1 可以把类型的属性变成可选的,如果传递的类型是嵌套很多层的,如何实现 Partial1 的递归版本,才能把所有嵌套的属性都变成可选的呢?
欢迎你在评论区分享你的答案,也欢迎你把这一讲的内容分享给你的同事和朋友们,我们下一讲再见。