You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

370 lines
17 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 加餐02深入TypeScript
你好,我是大圣。
在讲组件化的进阶开发篇之前我想在全家桶实战篇的最后用一讲的篇幅来专门聊一下TypeScript。希望你在学完这一讲之后能对TypeScript有一个全面的认识。
另外,今天我会设置很多实战练习,一边阅读一边敲代码的话,学习效果更好。而且,这次加餐中的全部代码都是可以在线完成的,建议你打开[这个链接](https://www.typescriptlang.org/play?#code/FAehAJC+9Q66MA3lHnrQMhGGO5QgB6F+E9gnU0AByhZBKA),把下面的每行代码都跟着敲一遍。
## TypeScript入门
对于TypeScript你首先要了解的是TypeScript 可以在JavaScript的基础上对变量的数据类型加以限制。TypeScript 中最基本的数据类型包括布尔、数字、字符串、null、undefined这些都很好理解。
在下面的代码中我们分别定义了这几个数据类型的变量你能看到当我们把number类型的变量price赋值字符串时就会报错当我们把数组 me 的第一个元素 me\[0\] 的值修改为数字时,也会报错。
```typescript
let courseName:string = '玩转Vue 3全家桶'
let price:number = 129
price = '89' //类型报错
let isOnline:boolean = true
let courseSales:undefined
let timer:null = null
let me:[string,number] = ["大圣",18]
me[0] = 1 //类型报错
```
当你不确定某个变量是什么类型时你可以使用any作为这个变量的类型。你可以用any标记任何属性可以修改任何数据访问任何方法也不会报错。也就是说在TypeScript中当你把变量的类型标记为any后这个变量的使用就和JavaScript没啥区别了错误只会在浏览器里运行的时候才会提示。
```typescript
let anyThing
let anyCourse :any = 1
anyCourse = 'xx'
console.log(anyCourse.a.b.c)
```
然后我们可以使用enum去定义枚举类型这样可以把类型限制在指定的场景之内。下面的代码中我们可以把课程评分限制在好、非常好和嘎嘎好三个值之内。
```typescript
enum 课程评分 {,非常好,嘎嘎好}
console.log(课程评分['好']===0)
console.log(课程评分[0]==='好')
let scores = [课程评分['好'], 课程评分['嘎嘎好'], 课程评分['非常好']]
```
然后我们可以通过学到的这些基础类型,通过组合的方式组合出新的类型,最常见的组合方式就是使用 | 实现类型联合。下面的代码中我们定义course1变量的类型为字符串或者数字赋值为这两个类型都不会报错还可以用来限制变量只能赋值为几个字符串的一个score的取值只能是代码中三个值之一。
```typescript
let course1 : string|number = '玩转vue 3'
course1 = 1
course1 = true // 报错
type courseScore = '好' | '非常好' | '嘎嘎好'
let score1 :courseScore = '好'
let score2 :courseScore = '一般好' // 报错
```
通过interface接口可以定义对象的类型限制。下面代码中我们定义了极客时间课程的类型课程名是字符串价格使用number\[\] 语法定义类型为数字组成的数组讲师头像是string或者boolean并且通过 ?设置为可选属性课程地址使用readonly设置为只读属性如果对课程地址进行修改就会报错。
```typescript
interface 极客时间课程 {
课程名字:string,
价格:number[],
受众:string,
讲师头像?:string|boolean,
readonly 课程地址:string
}
let vueCourse: 极客时间课程 = {
课程名字:'玩转Vue 3全家桶',
价格:[59,'139'],
讲师头像:false,
课程地址:"time.geekbang.org"
}
vueCourse.课程地址 = 'e3.shengxinjing.cn' // 报错
```
然后我们学一下函数的类型限制。其实函数的定义参数和返回值本质上也是变量的概念都可以进行类型的定义。下面的代码中我们定义了参数x和y是数字返回值也是数字的add函数定义好参数和返回值类型函数的类型自然也就确定了。
```typescript
function 函数名(参数:参数类型):返回值类型{} //大致语法
function add(x: number, y: number): number {
return x + y;
}
add(1, 2);
```
我们也可以使用变量的方式去定义函数,直接使用(参数类型) =>返回值类型的语法去定义add1的变量类型但是这样写出来的代码可读性稍差一些我更建议你使用type或者interface关键字去定义函数的类型。下面代码中的addType和addType1都是很好的定义函数类型的方式
```
let add1:(a:number,b:number)=>number = function(x: number, y: number): number {
return x + y;
}
type addType = (a:number,b:number)=>number
let add2:addType = function(x: number, y: number): number {
return x + y;
}
interface addType1{
(a:number,b:number):number
}
let add3:addType1 = function(x: number, y: number): number {
return x + y;
}
```
如果你的函数本来就支持多个类型的参数下面的代码中reverse函数既支持数字也支持字符串。**我们的要求是如果参数是数字,返回值也要是数字,参数是字符串返回值也只能是字符串**所以参数和返回值都用number|string就没法精确地限制这个需求。我们需要使用函数重载的方式定义多个函数的输入值和返回值类型更精确地限制函数的类型。我们可以在[Vue 3的源码](https://github.com/vuejs/vue-next/blob/master/packages/reactivity/src/ref.ts#L72)看到Vue 3中ref函数的重载写法
```typescript
function reverse(x: number): number
function reverse(x: string): string
function reverse(x: number | string): number | string | void {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}
```
这样TypeScript里如何限制一个变量和函数类型我们就大致入门了。这时候你肯定还有个疑问**日常开发中有很多浏览器上的变量和属性,这些怎么限制类型呢?**
关于宿主环境里的类型TypeScript全部都给我们提供了我们可以直接在代码中书写Window是window的类型HTMLElement是dom元素类型NodeList是节点列表类型MouseEvent是鼠标点击事件的类型……关于更多TypeScript的内置类型你可以在[TypeScript的源码](https://github.com/Microsoft/TypeScript/tree/main/src/lib)中看到:
```typescript
let w:Window = window
let ele:HTMLElement = document.createElement('div')
let allDiv: NodeList = document.querySelectorAll('div')
ele.addEventListener('click',function(e:MouseEvent){
const args:IArguments = arguments
w.alert(1)
console.log(args)
},false)
```
除了浏览器的API我们还会用到很多第三方框架比如Vue、Element3等等这些框架现在都提供了完美的类型可以直接使用。在[第18讲](https://time.geekbang.org/column/article/445880)中我们使用下面的代码Vue导出的Ref来限定数据是ref包裹的响应式数据
```typescript
import { ref ,Ref} from 'vue'
interface Todo{
title:string,
done:boolean
}
let todos:Ref = ref([{title:'学习Vue',done:false}])
```
## 泛型
那么聊完上面的内容你就已经能使用TypeScript实现很多项目的开发把所有变量和函数出现的地方都定义好类型就可以在编译阶段提前规避出很多报错。然而TypeScript的能力可不止于此**TypeScript可以进行类型编程这会极大提高TypeScript在复杂场景下的应用场景。**
然后我们来看一下TypeScript中的泛型这也是很多同学觉得TypeScript很难的最大原因。
首先我们看下面的代码我们定一个idientity0函数这个函数逻辑非常简单就是直接返回参数那么**我们怎么确定返回值的类型呢?**
因为输入值可以是任意属性所以我们只能写出identity0这个函数参数和返回值类型都是any但是明显不能满足我们的需求。我们需要返回值的类型和参数一致所以我们在函数名之后使用<>定一个泛型T你可以理解这个T的意思就是给函数参数定义了一个类型变量会在后面使用相当于【**type T = arg的类型**】返回值使用T这个类型就完成了这个需求。
```typescript
function identity0(arg: any): any {
return arg
}
// 相当于type T = arg的类型
function identity<T>(arg: T): T {
return arg
}
identity<string>('玩转vue 3全家桶') // 这个T就是string所以返回值必须得是string
identity<number>(1)
```
有了泛型之后我们就有了把函数参数定义成类型的功能我们就可以实现类似高阶函数的类型函数。下面的代码中我们使用keyof语法获得已知类型VueCourse5的属性列表相当于 name|price
```typescript
interface VueCourse5 {
name:string,
price:number
}
type CourseProps = keyof VueCourse5 // 只能是name和price选一个
let k:CourseProps = 'name'
let k1:CourseProps = 'p' // 改成price
```
keyof可以帮助我们拆解已有类型下一步我们需要使用extends来实现类型系统中的条件判断。我们定义类型函数ExtendsType接受泛型参数T后通过判断T是不是布尔值来返回不同的类型字符串我们就可以通过ExtendsType传入不同的参数去返回不同的类型。
```typescript
// T extends U ? X : Y 类型三元表达式
type ExtendsType<T> = T extends boolean ? "重学前端" : "玩转Vue 3"
type ExtendsType1 = ExtendsType<boolean> // type ExtendsType1='重学前端'
type ExtendsType2 = ExtendsType<string> // type ExtendsType2='玩转Vue 3'
```
extends相当于TypeScript世界中的条件语句然后in关键字可以理解为TypeScript世界中的遍历。下面的代码中我们通过 k in Courses语法相当于遍历了Courses所有的类型作为CourseObj的属性值的类型是number。
```typescript
type Courses = '玩转Vue 3'|'重学前端'
type CourseObj = {
[k in Courses]:number // 遍历Courses类型作为key
}
// 上面的代码等于下面的定义
// type CourseObj = {
// 玩转Vue 3: number;
// 重学前端: number;
// }
```
学完上面的语法,你就能完全搞懂[第18讲](https://time.geekbang.org/column/article/455487)里的getProperty函数。限制函数第二个参数只能是第一个参数的属性并且返回值的类型最后我们传递不存在的属性时TypeScript就会报错。
```typescript
// K extends keyof T限制K的类型必须是T的属性之一
// T[K]是值得类型
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
return o[name]
}
const coursePrice:CourseObj = {
"玩转Vue 3":129,
"重学前端":129
}
getProperty(coursePrice,'玩转Vue 3')
getProperty(coursePrice,'不学前端') // 报错
```
然后我再给你讲解最后一个关键字infer。<T>让我们拥有了给函数的参数定义类型变量的能力infer则是可以在extends之后的变量设置类型变量更加细致地控制类型。下面的代码中我们定义了ReturnType类型函数目的是返回传入函数的返回值类型。infer P的意思就是泛型T是函数类型并且这个函数类型的返回类型是P。
```typescript
type Foo = () => CourseObj
// 如果T是一个函数并且函数返回类型是P就返回P
type ReturnType1<T> = T extends ()=>infer P ?P:never
type Foo1 = ReturnType1<Foo>
```
## 实战练习
有了上面的基础后,我们来几个实战的练习。以下所有的练习都可以在代码最后找到答案,我建议你一定要自己实现一遍才能有最多的收获。
代码地址:[https://www.typescriptlang.org/docs/handbook/utility-types.html](https://www.typescriptlang.org/docs/handbook/utility-types.html)
下面的代码中我们首先定义类型Todo有title、desc和done三个属性
```typescript
interface Todo {
title: string
desc:string
done: boolean
}
```
首先第一题是我们需要实现类型函数Partial1返回的类型是Todo所有的属性都变成可选项。
```typescript
type partTodo = Partial1<Todo>
// 和下面类型一致鼠标移动到partTodo变量上也能看到
// type partTodo = {
// title?: string | undefined;
// desc?: string | undefined;
// done?: boolean | undefined;
// }
```
这一题的答案见下面的代码使用K in keyof T遍历所有T的属性后使用 ?标记为可选属性。
```typescript
type Partial1<T> = {
[K in keyof T]?:T[K]
}
```
TypeScript中还有很多类似的函数包括Pick、Omit、Diff等函数你都可以自行实现一遍更多工具类型函数你可以移步[TypeScript官方文档](https://www.typescriptlang.org/docs/handbook/utility-types.html)。你也可以结合下面的代码工具函数的实现,到留言区中讨论一下分别实现了什么功能。
```typescript
type Exclude1<T, K> = T extends K ? never : T
type Pick1<T, K extends keyof T> = {
[P in K]: T[P]
}
type Concat1<T extends any[], U extends any[]> = [...T, ...U]
```
最后我们再来一个实战的练习在实际项目开发中除了JavaScript、浏览器和第三方框架的类型还有一个很重要的场景就是后端返回的数据类型。我们需要根据开发文档去定义好每个请求的类型在下面的代码中request作为发送请求的函数可以传递url是字符串。**那我们该如何定义Interface API使其能够限制request只能有buy和comment两个请求地址并且comment请求的参数中message是必传项呢**
```typescript
import axios from 'axios'
function request(url:string,obj:any){
return axios.post(url,obj)
}
interface Api{
}
request('/course/buy',{id:1})
request('/course/comment',{id:1,message:'嘎嘎好看'})
request('/course/comment',{id:1}) //如果message必传 怎么类型提醒缺少参数
request('/course/404',{id:1}) //接口不存在 类型怎么需要报错
```
记得要先尝试自己实现一下答案就在下面的代码中。在API类型中我们定义了buy和comment两个属性分别设置了当前请求所需要的参数都是必选项。然后我们通过在request中使用泛型T限制url通过Api\[T\] 限制传递的参数这样我们就得到了下面的报错示意图在编译阶段就能通知你缺少message属性并且404请求不存在这可以极大提高我们开发的体验和效率。
```typescript
import axios from 'axios'
interface Api{
'/course/buy':{
id:number
},
'/course/comment':{
id:number,
message:string
}
}
function request<T extends keyof Api>(url:T,obj:Api[T]){
return axios.post(url,obj)
}
request('/course/buy',{id:1})
request('/course/comment',{id:1,message:'嘎嘎好看'})
request('/course/comment',{id:1}) //如果message必传 怎么类型提醒缺少参数
request('/course/404',{id:1}) //接口不存在 类型怎么需要报错
```
![图片](https://static001.geekbang.org/resource/image/0c/8e/0c52cee36238831a10e4004d6536f18e.png?wh=1420x612)
更多类型的练习,你可以访问[type-challenges](https://github.com/type-challenges/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的递归版本才能把所有嵌套的属性都变成可选的呢
欢迎你在评论区分享你的答案,也欢迎你把这一讲的内容分享给你的同事和朋友们,我们下一讲再见。