危险

为之则易,不为则难

0%

03_TypeScript

今日目标

✔ 掌握 TypeScript 的基本使用,例如 type、interface、泛型函数等。

✔ 掌握 TypeScript 在 Vue 项目中的应用。

TypeScript 概述

概念

TS是JS的超集

  • TypeScript 是微软开发的编程语言,它是 JavaScript 的超集,可以在任何运行 JavaScript 的地方运行,官方文档中文文档,不再维护

  • TypeScript = Type + JavaScript(在 JS 基础之上,为 JS 添加了类型支持/类型检测)。

1
let age1: number = 18 // TS 代码 => 有明确的类型,即 number(数值类型)

优势

  • 背景:JS 的类型系统存在“先天缺陷”,是弱类型语言,而代码中的大部分错误都是类型错误(TypeError),这些经常出现的错误,导致了在使用 JS 进行项目开发时,增加了找 Bug、改 Bug 的时间,严重影响开发效率。

  • 发现错误的时机更早

    • 对于 JS 来说:需要等到代码真正去执行的时候才能发现错误(晚);

    • 对于 TS 来说:在代码编译的时候(代码执行前)就可以发现错误(早),配合 VSCode 等开发工具,发现错误的时机可以提前到在编写代码的时候,减少找 Bug、改 Bug 时间。

  • 代码提示,随时随地的安全感,增强了开发体验。

  • 支持最新的 ECMAScript 语法,优先体验最新的语法,让你走在前端技术的最前沿。

  • Vue3 源码使用 TS 重写、Angular 默认支持 TS、React 与 TS 完美配合,TypeScript 已成为大中型前端项目的首选编程语言,前端最新的开发技术栈离不开 TS,例如 React(TS + Hooks),Vue(TS + Vue3)。

1
2
3
4
5
// 使用 JavaScript:在 VSCode 里面写代码;在浏览器中运行代码,发现错误【晚】。
// 使用 TypeScript:在 VSCode 里面写代码;写代码的同时,就会发现错误【早】;在浏览器中运行代码。
let num = 123
num = 'abc'
num.toFixed(2) // Uncaught TypeError: num.toFixed is not a function

编译 TypeScript

安装编译 TS 的工具

  • 问题:为什么要安装编译 TS 的工具包?

  • 回答:Node.js/浏览器,只认识 JS 代码,不认识 TS 代码,因此需要先将 TS 代码转化为 JS 代码,然后才能运行。

  • 安装命令:npm i -g typescript 或者 yarn global add typescript

  • 验证是否安装成功:tsc –v(查看 TypeScript 的版本)。

TS 编译

编译并运行 TS 代码

  1. 创建 hello.ts 文件(注意:TS 文件的后缀名为 .ts)。

  2. 将 TS 编译为 JS:在终端中输入命令,tsc hello.ts(此时,在同级目录中会出现一个同名的 JS 文件)。

  3. 执行 JS 代码:在终端中输入命令,node hello.js

  4. 说明:所有合法的 JS 代码都是 TS 代码,有 JS 基础只需要学习 TS 的类型即可。

  5. 注意:由 TS 编译生成的 JS 文件,代码中就没有类型信息了。

1
2
3
4
# 监听 index.ts 文件的变化并编译
tsc -w index.ts # 窗口 1
# 运行编译后的代码
nodemon index.js # 窗口 2

TypeScript 基础

类型注解

目标

能够理解什么是 TypeScript 的类型注解。

内容

  • TypeScript 是 JS 的超集,TS 提供了 JS 的所有功能,并且额外的增加了:类型系统,JS 虽然也有类型(比如,number/string 等),但 JS 并不会对类型进行校验和提示。

  • TypeScript 类型系统的主要优势:校验和提示

1
let age: number = 18
  • 说明:代码中的 :number 就是类型注解

  • 作用:为变量添加类型约束,比如上述代码中,约定变量 age 的类型为 number 类型。

  • 解释:约定了什么类型,就只能给变量赋值该类型的值,也会出现该类型相关的提示

1
2
// 错误原因:将 string 类型的值赋值给了 number 类型的变量,类型不一致
let age: number = '18'

原始类型

目标

能够理解 TS 中原始类型的使用。

内容

可以将 TS 中的常用基础类型细分为两类,分别是 JS 已有类型和 TS 新增类型。

  • JS 已有类型。
1
2
3
4
5
// 原始类型:`number/string/boolean/null/undefined/symbol/bigint`
const age: number = 18
const myName: string = 'Ifer'
const isLoading: boolean = false
// ...
  • TS 新增类型。

    a,联合类型、自定义类型(类型别名)、接口、元组、字面量类型、枚举、void、any 等。

    b,注意:TS 中的原始类型和 JS 中写法一致;TS 中的对象类型在 JS 类型基础上更加细化,每个具体的对象(比如数组、对象、函数)都有自己的类型语法。

数组类型

目标

掌握 TS 中数组类型的两种写法。

内容

1
2
// 写法 1
let numbers: number[] = [1, 3, 5]
1
2
3
// 写法 2
let strings: Array<string> = ['a', 'b', 'c']
strings.push('d') // 后续 push 的数据也必须是字符串

联合类型

目标

能够通过联合类型将多个类型组合成一个类型。

内容

  • 需求:数组中既有 number 类型,又有 string 类型,这个数组的类型应该如何写?
1
2
// 定义一个数组,数组中可以有数字或者字符串, 需要注意 | 的优先级
let arr: (number | string)[] = [1, 'abc', 2]
  • 解释:|(竖线)在 TS 中叫做联合类型,即由两个或多个其他类型组成的类型,表示可以是这些类型中的一种。

  • 注意:这是 TS 中联合类型的语法,只有一根竖线,不要与 JS 中的或(||)混淆了。

  • 场景:定时器的初始变量定义。

1
2
3
4
// 有问题的代码
let timer = null
// Type 'number' is not assignable to type 'null'.
timer = setInterval(() => {})
1
2
3
// 解决,思考除了下面方法还有其他办法吗?
let timer: number | null = null
timer = setInterval(() => {})
1
2
3
4
5
6
7
// 忽略
// 通过 tsc --init 命令可以生成配置文件
// 通过 strictNullChecks 指定为 true 可以开启对 null 和 undefined 的检测
// 即便开启了检测,当 null 赋值给某个变量时,这个变量会被推断为 any 类型
// !通过 noImplicitAny 指定为 false 可以禁用 any 类型,此时 null 赋值给某个变量时将会是 null 类型
let timer: number | null = null
timer = setInterval(() => {}, 1000)

类型别名

目标

能够使用类型别名给类型起别名。

内容

  • 类型别名作用:为任意类型起别名,别名甚至可以是中文。
1
2
3
4
5
type s = string
const myName: s = 'ifer'

type 字符串类型 = string
const myAddress: 字符串类型 = '河南老乡~'
  • 使用场景:当同一类型(复杂)且可能被多次使用时,可以通过类型别名,简化该类型的使用
1
2
3
4
type CustomArray = (number | string)[]

let arr1: CustomArray = [1, 'a', 3, 'b']
let arr2: CustomArray = ['x', 'y', 6, 7]
  • 解释说明。

    a,使用 type 关键字来创建自定义类型。

    b,类型别名(比如,此处的 CustomArray)可以是任意合法的变量名称。

    c,推荐使用大写字母开头。

    d,创建类型别名后,直接使用该类型别名作为变量的类型注解即可。

函数类型

基本使用

目标

能够给函数指定类型。

内容

函数的类型实际上指的是:函数参数返回值 的类型,为函数指定类型有如下两种方式。

  • 单独指定参数、返回值的类型。
1
2
3
4
5
6
7
8
9
// 函数声明
function add(num1: number, num2: number): number {
return num1 + num2
}

// 箭头函数
const add = (num1: number, num2: number): number => {
return num1 + num2
}
  • 同时指定参数、返回值的类型。
1
2
3
4
5
6
// 解释:可以通过类似箭头函数形式的语法来为函数添加类型,注意这种形式只适用于函数表达式。
type AddFn = (num1: number, num2: number) => number

const add: AddFn = (num1, num2) => {
return num1 + num2
}

void 类型

目标

能够了解 void 类型的使用。

内容
  • 基础使用。
1
2
3
4
// 注意:在没有开始 strictNullChecks 模式的情况下,可以把 null 和 undefined 赋值给任意类型
// 如何开启:通过 tsc --init 生成配置文件,默认就会开启 strictNullChecks
// let temp: void = null // ok
let temp: void = undefined // ok
  • 如果函数没有返回值,那么函数返回值类型为:void
1
2
3
4
function greet(name: string): void {
console.log('Hello', name)
// return undefined // 默认有这么一句
}
  • 注意:如果一个函数明确了返回类型是 undefined,则必须显示的 return undefined
1
2
3
const add = (): undefined => {
return undefined
}

可选参数

目标

能够使用 ? 给函数指定可选参数类型。

内容
  • 使用函数实现某个功能时,参数可以传也可以不传,这种情况下,在给函数参数指定类型时,就用到可选参数了。

  • 比如,数组的 slice 方法,可以 slice() 也可以 slice(1) 还可以 slice(1, 3)

  • 可选参数语法:在可传可不传的参数名称后面添加 ?(问号)。

1
2
3
4
// start、end 可传可不传,传就传 number 类型
function mySlice(start?: number, end?: number): void {
console.log('起始索引:', start, '结束索引:', end)
}
  • 注意:可选参数只能出现在参数列表的最后,也就是说可选参数后面不能再出现必选参数。

参数默认值

目标

能够给函数指定默认值。

内容

通过赋值符号(=)可以给参数执行默认值,注意:参数默认值和可选参数互斥的,只能指定其中一种。

1
2
// Error: Parameter cannot have question mark and initializer
function mySlice(start: number = 0, end?: number = 0) {}
1
2
// 可选参数
function mySlice(start: number = 0, end?: number) {}
1
2
// 默认值
function mySlice(start: number = 0, end: number = 0) {}

对象类型

基本使用

目标

掌握对象类型的基本使用。

内容

JS 中的对象是由属性和方法构成的,而 TS 对象的类型就是在描述数据的结构(有什么样类型的属性和方法)。

  • 基本使用。
1
const person: object = {}
  • 另一种使用方式。
1
2
// 左边的 {} 表示类型(严格来说应该是对象字面量类型),右边的 {} 表示值
let person: {} = {}
  • 可以精确描述对象里面具体内容的类型。
1
2
3
4
// 要求必须指定 string 类型的 name 属性,左右两边数量保持一致
const person: { name: string } = {
name: '同学',
}
1
2
3
4
5
6
const obj = {
name: '同学',
age: 18,
}
// 右边是变量,在满足左边声明的前提下(右边内容可以比左边多)
const person: { name: string } = obj
1
2
// 字符串比较特殊,满足左边的类型要求即可
const str: { length: number } = 'hello'
  • 描述对象中方法的类型。
1
2
3
4
5
6
7
8
9
10
// 在一行代码中指定对象的多个属性类型时,使用 `;`(分号)来分隔
// 单独制定函数的参数和返回值
// const person: { name: string; add(n1: number, n2: number): number } = {
// 可以统一指定函数的参数和返回值
const person: { name: string; add: (n1: number, n2: number) => number } = {
name: '同学',
add(n1, n2) {
return n1 + n2
},
}
  • 也可以通过换行来分隔多个属性类型,去掉 ;
1
2
3
4
5
6
7
8
9
const person: {
name: string
add(n1: number, n2: number): number
} = {
name: '同学',
add(n1, n2) {
return n1 + n2
},
}
  • 定义对象类型时也可以结合类型别名来使用。
1
2
3
4
5
6
7
8
9
10
type Person = {
name: string
add(n1: number, n2: number): number
}
const person: Person = {
name: '同学',
add(n1, n2) {
return n1 + n2
},
}
小结
  • 使用 {} 来描述对象/数据结构。

  • 属性采用 属性名: 类型 的形式。

  • 方法采用 方法名(): 返回值类型 的形式。

对象可选属性

  • 对象的属性或方法,也可以是可选的,此时就用到可选属性了。

  • 比如,我们在使用 axios({ ... }) 时,如果发送 GET 请求,method 属性就可以省略。

  • 可选属性的语法与函数可选参数的语法一致,都使用 ? 来表示。

1
2
3
4
5
6
7
8
type Config = {
url: string
method?: string
}

function myAxios(config: Config) {
console.log(config)
}

练习

创建两个学生对象:包含姓名、性别、成绩、身高、学习、打游戏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Student = {
name: string
gender: string
score: number
height: number
study(): void
play: (name: string) => void
}

const stu: Student = {
name: 'xxx',
gender: 'man',
score: 88,
height: 178,
study() {
console.log('学学学')
},
// play() 这里不写参数,也不会马上报错,但 stu.play() 调用的时候就知道了
play(name) {},
}

接口

当一个对象类型被多次使用时,一般会使用接口(interface)来描述对象的类型,达到复用的目的。

  • 使用 interface 关键字来声明接口。

  • 接口名称(比如,此处的 IPerson),可以是任意合法的变量名称,推荐以 I 开头。

  • 声明接口后,直接使用接口名称作为变量的类型。

  • 因为每一行只有一个属性类型,因此,属性类型后没有 ;(分号)。

1
2
3
4
5
6
7
8
9
10
11
12
13
interface IStudent {
name: string
gender: string
study(): void
}

const stu: IStudent = {
name: 'xxx',
gender: 'man',
study() {
console.log('学学学')
},
}

接口继承

如果两个类型之间有相同的属性或方法,可以将公共的属性或方法抽离出来,通过继承来实现复用

  • type 方式。
1
2
3
4
5
6
7
8
9
type Point2D = {
x: number
y: number
}
type Point3D = {
x: number
y: number
z: number
}
  • interface 方式。
1
2
3
4
5
6
7
8
9
interface Point2D {
x: number
y: number
}
// 使用 `extends`(继承)关键字实现了接口 Point3D 继承 Point2D
// 继承后,Point3D 就有了 Point2D 的所有属性和方法(此时,Point3D 同时有 x、y、z 三个属性)
interface Point3D extends Point2D {
z: number
}

interface vs type

相同点
  • 都可以描述对象或者函数。
1
2
3
4
5
6
// interface 描述对象
interface IPerson {
name: string
age: number
}
const p: IPerson = { name: 'ifer', age: 18 }
1
2
3
4
5
6
7
// interface 描述函数
interface ISetPerson {
(name: string, age: number): void
}
const setPerson: ISetPerson = (name, age) => {}

setPerson('ifer', 18)
1
2
3
4
5
6
// type 描述对象
type TPerson = {
name: string
age: number
}
const p: TPerson = { name: 'ifer', age: 18 }
1
2
3
4
5
6
7
// type 描述函数
type TSetPerson = {
(name: string, age: number): void
}
const setPerson: TSetPerson = (name, age) => {}

setPerson('ifer', 18)
  • 都允许拓展,语法不一样。
1
2
3
4
5
6
7
8
9
10
11
12
// interface extends interface
interface IName {
name: string
}
interface IPerson extends IName {
age: number
}

const p: IPerson = {
name: 'ifer',
age: 18,
}
1
2
3
4
5
6
7
8
9
10
// interface extends type
type TName = { name: string }
interface IPerson extends TName {
age: number
}

const p: IPerson = {
name: 'ifer',
age: 18,
}
1
2
3
4
5
6
7
8
// type & type
type TName = { name: string }
type TPerson = { age: number } & TName

const p: TPerson = {
name: 'ifer',
age: 18,
}
1
2
3
4
5
6
7
8
9
10
// type & interface
interface IName {
name: string
}
type TPerson = { age: number } & IName

const p: TPerson = {
name: 'ifer',
age: 18,
}
不同点

type 除了可以描述对象或函数,实际上可以为任意类型指定别名。

1
type NumStr = number | string

相同的 interface 声明能够合并,相同的 type 声明会报错。

1
2
3
4
5
6
7
8
9
10
interface IPerson {
name: string
}
interface IPerson {
age: number
}
const p: IPerson = {
name: 'ifer',
age: 18,
}

总结:一般使用 interface 来描述对象结构,用 type 来描述类型关系。

元组类型

  • 使用 number[] 的缺点:不严谨,因为该类型的数组中可以出现任意多个数字。

  • 元组 Tuple,元组是特殊的数组类型,它能确定元素的个数以及特定索引对应的类型

1
const position: [number, number] = [39.5427, 116.2317]
  • 解释说明。

    a,元组类型可以确切地标记出有多少个元素,以及每个元素的类型。

    b,该示例中,元素有两个元素,每个元素的类型都是 number。

1
2
// 可以给元组中的元素起别名
const arrTuple: [height: number, age: number, salary: number] = [170, 20, 17500]

类型推论

  • 在 TS 中,某些没有明确指出类型的地方,TS 的类型推论机制会帮助提供类型

  • 换句话说:由于类型推论的存在,这些地方,类型注解可以省略不写。

  • 常见的发生类型推论的 2 种场景:声明变量并初始化时;决定函数返回值时。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 变量 age 的类型被自动推断为:number
let age = 18

const obj = {
name: 'ifer',
age: 18,
show() {},
}

// 函数返回值的类型被自动推断为:number
function add(num1: number, num2: number) {
return num1 + num2
}
  • 推荐:代码写熟了之后,有类型推论的情况下可以省略类型注解,充分利用 TS 类型推论的能力,提升开发效率。

  • 技巧:如果不知道类型,可以通过鼠标放在变量名称上,利用 VSCode 的提示来查看类型。

  • 建议:在 VSCode 中写代码的时候,多看方法、属性的类型,养成写代码看类型的习惯,例如 const oDiv = document.createElement('div')

字面量类型

基本使用

思考以下代码,两个变量的类型分别是什么?

1
2
let str1 = 'Hello TS'
const str2 = 'Hello TS'

通过 TS 类型推论机制,可以得到答案:变量 str1 的类型为:string,变量 str2 的类型为:’Hello TS’。

  • str1 是一个变量,它的值可以是任意字符串,所以类型为:string。

  • str2 是一个常量,它的值不能变化只能是 ‘Hello TS’,所以,它的类型为:’Hello TS’(字符串字面量类型)。

  • 注意:此处的 ‘Hello TS’,就是一个字符串字面量类型,也就是说某个特定的字符串也可以作为 TS 中的类型。

  • 任意的 JS 字面量都可以作为类型使用,例如 { name: 'jack' }[]18'abc'falsefunction() {} 等。

使用方式和场景

  • 使用方式:字面量类型常配合联合类型一起使用

  • 使用场景:用来表示一组明确的可选值列表,比如在贪吃蛇游戏中,游戏方向的值只能是上、下、左、右中的一个。

1
2
3
4
5
type Direction = 'up' | 'down' | 'left' | 'right'
function changeDirection(direction: Direction) {
console.log(direction)
}
changeDirection('up') // 调用函数时,会有类型提示
  • 解释:参数 direction 的值只能是 up/down/left/right 中的任意一个。

  • 优势:相比于 string 类型,使用字面量类型更加精确、严谨

  • 其他应用场景,性别和 Redux 中的 Action 等等。

1
2
type Gender = '男' | '女'
const zs: Gender = '男'
1
2
3
4
5
6
7
8
9
type Action = {
type: 'TODO_ADD' | 'TODO_DEL' | 'TODO_CHANGE' | 'TODO_FIND'
}

function reducer(state, action: Action) {
switch (action.type) {
case 'TODO_ADD': // 这里会自动具有提示
}
}

枚举类型

基本使用

  • 枚举的功能类似于字面量类型+联合类型组合的功能,也可以表示一组明确的可选值。

  • 枚举:定义一组命名常量,它描述一个值,该值可以是这些命名常量中的一个。

  • 使用 enum 关键字定义枚举,约定枚举名称以大写字母开头。

  • 枚举中的多个值之间通过 ,(逗号)分隔,定义好枚举后,直接使用枚举名称作为类型注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建枚举
enum Direction {
Up,
Down,
Left,
Right,
}

// 可以当做类型使用枚举
function changeDirection(direction: Direction) {
console.log(direction)
}

// 也可以当做值使用枚举
// 调用函数时,需要传入:枚举 Direction 成员的任意一个,类似于 JS 中的对象,直接通过点(.)语法 访问枚举的成员
changeDirection(Direction.Up)

数字枚举

  • 问题:我们把枚举成员作为了函数的实参,它的值是什么呢?

  • 解释:通过将鼠标移入 Direction.Up,可以看到枚举成员 Up 的值为 0。

  • 注意:枚举成员是有值的,默认为:从 0 开始自增的数值。

  • 我们把枚举成员的值为数字的枚举称为:数字枚举

  • 当然,也可以通过“等号”给枚举中的成员指定初始值,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Down -> 11、Left -> 12、Right -> 13
enum Direction {
Up = 10,
Down,
Left,
Right,
}

enum Direction {
Up = 2,
Down = 4,
Left = 8,
Right = 16,
}
console.log(Direction['Up']) // 2
// 也可以反向操作
console.log(Direction[2]) // Up

实现原理

  • 枚举类型比较特殊,不仅仅可以用作类型,还可以当做值使用,因为枚举成员都是有值的。

  • 也就是说,其他的类型会在编译为 JS 代码时自动移除,但是,枚举类型会被编译为 JS 代码

  • 说明:枚举与前面讲到的字面量类型 + 联合类型组合的功能类似,都用来表示一组明确的可选值列表。

  • 推荐:字面量类型 + 联合类型组合的方式,因为相比枚举,这种方式更加直观、简洁、高效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum Direction {
Up = 2,
Down = 4,
Left = 8,
Right = 16,
}

// 会被编译为以下 JS 代码:
var Direction
;(function (Direction) {
Direction[(Direction['Up'] = 2)] = 'Up'
Direction[(Direction['Down'] = 4)] = 'Down'
Direction[(Direction['Left'] = 8)] = 'Left'
Direction[(Direction['Right'] = 16)] = 'Right'
console.log(Direction)
})(Direction || (Direction = {}))

字符串枚举

  • 定义:枚举成员的值是字符串称为字符串枚举。

  • 注意:字符串枚举没有自增长行为,因此,字符串枚举的每个成员必须有初始值

1
2
3
4
5
6
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}

🧐 具体的使用案例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum Gender {
女,
男,
}
type User = {
name: string
age: number
// gender: '男' | '女' // 但后台需要 0 和 1
gender: Gender
}

const user: User = {
name: 'ifer',
age: 18,
gender: Gender.男,
}

类型断言

有时候你会比 TS 更加明确一个值的类型,此时可以使用类型断言来指定更具体的类型,比如根据 ID 选择 a 标签。

1
2
// 注意 document.querySelector('a') 这种写法会自动推断出是 HTMLLinkElement 类型
const oLink = document.getElementById('link')
  • 注意:该方法返回的类型是 HTMLElement,该类型只包含所有标签公共的属性或方法,不包含 a 标签特有的 href 等属性,这个类型太宽泛(不具体),无法操作 href 等 a 标签特有的属性或方法。

  • 解决方式:这种情况下就需要使用类型断言指定更加具体的类型。

1
const oLink = document.getElementById('link') as HTMLAnchorElement
  • 解释说明。

    a,使用 as 关键字实现类型断言。

    b,关键字 as 后面的类型是一个更加具体的类型(HTMLAnchorElement 是 HTMLElement 的子类型)。

    c,通过类型断言,oLink 的类型变得更加具体,这样就可以访问 a 标签特有的属性或方法了。

  • 另一种语法,使用 <> 语法,这种语法形式不常用知道即可。

1
const oLink = <HTMLAnchorElement>document.getElementById('link')
  • 技巧:打开浏览器控制台,选中标签,通过 $0.__proto__ 可以获取 DOM 元素的类型。

🤔 注意:只有两个有“关系”的类型间才能进行断言,例如你可以将一个联合类型(string|number)断言为其中某一更加具体的类型(number),将一个宽泛的类型(Element)断言为更加具体的类型(HTMLDivElement)。

typeof

  • JS 中的 typeof 可以在运行时判断类型,TS 中的 typeof 可以在编译时获取类型。
1
2
3
4
5
6
7
8
interface Person {
name: string
age: number
}
const person: Person = { name: 'ifer', age: 18 }

// 获取 person 的类型,得到的就是 Person 接口类型
type p = typeof person
  • TS 中 typeof 的使用场景:根据已有变量的值,获取该值的类型,来简化类型书写。
1
2
3
4
5
const p = { x: 1, y: 2 }
function formatPoint(point) {} // 没有提示
function formatPoint(point: { x: number; y: number }) {} // 有提示,写法麻烦
// 使用 `typeof` 操作符来获取变量 p 的类型,结果与上面对象字面量的形式相同
function formatPoint(point: typeof p) {} // 推荐
  • 注意 typeof 出现在类型注解的位置(参数名称的冒号后面,区别于 JS 代码)。

keyof

作用:获取接口、对象(配合 typeof)、类等的所有属性名组成的联合类型。

1
2
3
4
5
6
7
// 接口
interface Person {
name: string
age: number
}
type K1 = keyof Person // "name" | "age"
type K2 = keyof Person[] // "length" | "toString" | "pop" | "push" | "concat" | "join"
1
2
3
4
5
6
7
8
9
// 对象(要配合 typeof 才能使用)
const obj = { name: 'ifer', age: 18 }
/* type newobj = typeof obj
type keyofObj = keyof newobj // "name" | "age" */

// 简写
type keyofObj = keyof typeof obj // "name" | "age"
let s1: keyofObj = 'name' // ok
let s2: keyofObj = 'xxx' // error

下面的代码了解即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 类
class User {
// constructor(public username: string, public age: number) {}
public username: string
public age: number
constructor(username: string, age: number) {
this.username = username
this.age = age
}
}

type UserInfo = keyof User // "username" | "age"
const s: UserInfo = 'username' // ok
1
2
3
4
5
6
7
8
9
10
11
// 基本类型
type K1 = keyof boolean // 'valueOf'
type T2 = keyof number // 'toString' | 'toFixed' | ...
type T3 = keyof any // string | number | symbol

// 枚举
enum HttpMethod {
GET,
POST,
}
type Method = keyof typeof HttpMethod // 'GET' | 'POST'

特殊类型

any

  • 原则:不推荐使用 any!这会让 TypeScript 变为 “AnyScript”(失去 TS 类型保护的优势)。

  • 因为当值的类型为 any 时,可以对该值进行任意操作,即使可能存在错误,并且不会有代码提示。

1
2
3
let num: any = 8 // 任意类型,不对类型进行校验
num.toFixed() // 没有提示
num = 'xxx' // 可以赋任意值(即可以把任意值给 any 类型)
  • 尽可能的避免使用 any 类型,除非临时使用 any 来“避免”书写很长、很复杂的类型,或者有些参数就是可以使用任何类型,例如 console.log()

  • 其他隐式具有 any 类型的情况(因为不推荐使用 any,所以下面两种情况下都应该提供类型)。

    a,声明变量不提供类型也不提供默认值。

    b,函数参数不加类型。

unknow

  • unknown: 任意类型,更安全的 any 类型。
1
2
3
4
5
let num: unknown = 88
num = 'abc'
console.log(num)
num() // error: 不能调用方法
console.log(num.length) // error: 不能访问属性
  • 可以使用类型收窄来处理 unknown 类型。
1
2
3
4
5
6
let num: unknown = 88
if (typeof num === 'string') {
console.log(num.length)
} else if (typeof num === 'function') {
num()
}

并不是所有的类型都可以进行收窄。

1
2
3
4
5
6
7
let num = 'hello' // num 的类型已经确定就是 string 类型
if (typeof num === 'string') {
console.log(num.length)
} else if (typeof num === 'function') {
// 如果再等于了 function 类型,那是不可能的,所以 num 被推断为了 never 类型
num() // Error
}
  • unknown 类型可以配合断言使用。
1
2
3
let num: unknown = 88
let len = (num as string).length
console.log(len)

比较

  • 任何类型可以给 any,any 也可以给任何类型。
1
2
let temp: any = 'hello'
let str: string = temp // ok
  • 任何类型可以给 unknown,unknown 只能给 unknown 或 any 类型。
1
2
3
4
5
let temp: unknown = 'hello'
// 把一个不知道的类型给了 string 类型的变量 str
// let str: string = temp // error
// 解决,配合类型断言
let str: string = temp as string // ok
  • 测试:如何把 string 类型的变量赋值给 number 类型?
1
2
3
let temp: string = '888'
// 把 string 类型的变量给了 number 类型的变量 num,显然是有问题的
let num: number = temp
  • 解决方式一。
1
2
3
let temp: string = '888'
// 先断言为 any,利用 any 可以给任何类型的特点
let num: number = temp as any
  • 解决方式二。
1
2
3
let temp: string = '888'
// 不能直接断言 string 为 number,但可以断言 unknown 为 number
let num: number = temp as unknown as number

never

不可能实现的类型,例如下面的 Test 就是 never。

1
type Test = number & string
1
2
3
4
// 也可以当做函数的返回值,表示不会执行到头
function test(): never {
throw new Error('Error')
}

null 和 undefined

1
2
3
4
5
6
7
let str: string = 'ifer'

// 默认情况下,tsconfig.json 中的 strictNullChecks 的值为 false
// undefined 和 null 是其他类型的子类型,也就是可以作为其他类型的值存在

str = undefined
str = null

函数重载

1
2
3
function greet(name: string): string {
return `Hello ${name}`
}

需求:改造上面的函数,输入 [‘a’, ‘b’, ‘c’],输出 [‘Hello a’, ‘Hello b’, ‘Hello c’]。

方法 1,使用联合类型实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function greet(name: string | string[]): string | string[] {
if (typeof name === 'string') {
return `Hello ${name}`
} else if (Array.isArray(name)) {
return name.map((name) => `Hello ${name}`)
}
throw new Error('异常')
}
const r = greet(['a', 'b', 'c'])
console.log(r) // r 是一个联合类型

// 期望是 string[] 类型,可以通过断言
// const len = (r as string[]).length
// console.log(len)

// 了解
// 泛型断言
// const len = (<string[]>r).length
// console.log(len)
// or
// const len = (<Array<string>>r).length
// console.log(len)

方法 2,使用函数重载实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 一个函数可以有多个重载签名
// !重载签名:包含了函数的参数类型和返回值类型,但不包含函数体
function greet(name: string): string
function greet(name: string[]): string[]

// 一个函数只能有一个实现签名
// !实现签名:参数和返回值要覆盖上面的情况(更通用),且包含了函数体
function greet(person: unknown): unknown {
if (typeof name === 'string') {
return `Hello ${name}`
} else if (Array.isArray(name)) {
return name.map((name) => `Hello ${name}`)
}
throw new Error('异常')
}

console.log(greet(['a', 'b', 'c']))

TypeScript 泛型

基本介绍

  • 泛型:定义时宽泛、不确定的类型,需要使用者去主动传入。

  • 需求:创建一个 id 函数,传入什么数据类型就返回该数据类型本身(也就是说,参数和返回值类型相同)。

1
2
3
function id(value: number): number {
return value
}
  • 比如,id(10) 调用以上函数就会直接返回 10 本身,但是,该函数只接收数值类型,无法用于其他类型。

  • 为了让函数能够接受任意类型,可以将参数类型修改为 any,但是,这样就失去了 TS 的类型保护,类型不安全。

1
2
3
function id(value: any): any {
return value
}

泛型函数

  • 定义。

    a,语法:在函数名称的后面添加 <>(尖括号),尖括号中添加类型变量

    b,类型变量:一种特殊类型的变量,它处理类型而不是值,比如下面案例中的 Type。

    c,该类型变量相当于一个类型容器,能够捕获用户提供的类型(具体是什么类型由用户调用该函数时指定)。

    d,因为 Type 是类型,因此可以将其作为函数参数和返回值的类型,表示参数和返回值具有相同的类型。

    e,类型变量 Type,可以是任意合法的变量名称,一般简写为 T。

1
2
3
function id<Type>(value: Type): Type {
return value
}
1
2
3
function id<T>(value: T): T {
return value
}
  • 调用。

    a,语法:在函数名称的后面添加 <>(尖括号),尖括号中指定具体的类型,比如 number 或 string 等。

    b,当传入类型 number 后,这个类型就会被函数声明时指定的类型变量 Type 捕获到。

    c,此时,Type 的类型就是 number,所以,函数 id 参数和返回值的类型也都是 number。

    d,同样,如果传入类型 string,函数 id 参数和返回值的类型就都是 string。

    e,这样,通过泛型就做到了让 id 函数与多种不同的类型一起工作,实现了复用的同时保证了类型安全

1
2
const num = id<number>(10)
const str = id<string>('a')

简化泛型函数调用

1
2
let num = id(10) // 省略 <number> 调用函数
let str = id('a') // 省略 <string> 调用函数
  • 在调用泛型函数时,可以省略 <类型> 来简化泛型函数的调用

  • 此时,TS 内部会采用一种叫做类型参数推断的机制,来根据传入的实参自动推断出类型变量 Type 的类型。

  • 比如,传入实参 10,TS 会自动推断出变量 num 的类型 number,并作为 Type 的类型。

  • 推荐:使用这种简化的方式调用泛型函数,使代码更简短,更易于阅读。

  • 说明:当编译器无法推断类型或者推断的类型不准确时,就需要显式地传入类型参数

泛型约束

  • 泛型函数的类型变量 Type 可以代表任意类型,这导致访问泛型类型定义的数据属性时会没有提示,或者报错。

  • 比如,id('a') 调用函数时获取参数的长度。

1
2
3
4
5
6
function id<Type>(value: Type): Type {
console.log(value.length) // Property 'length' does not exist on type 'Type'
return value
}

id(['a', 'b'])
  • 解释:Type 可以代表任意类型,无法保证一定存在 length 属性,比如 number 类型就没有 length。

  • 解决:需要为泛型添加约束来收缩类型(缩窄类型取值范围)。

  • 主要有两种方式:1. 指定更加具体的类型,2. 通过 extends 关键字配合 interface 来添加约束。

指定更加具体的类型

比如,将类型修改为 Type[](Type 类型的数组),因为只要是数组就一定存在 length 属性,因此就可以访问了。

1
2
3
4
5
6
7
// 其实泛型 Type 约束的是数组里面的元素
function id<Type>(value: Type[]): Type[] {
console.log(value.length)
return value
}

id<string>(['a', 'b'])

添加泛型约束

  • 创建描述约束的接口 ILength,该接口要求提供 length 属性。

  • 通过 extends 关键字使用该接口,为泛型(类型变量)添加约束。

  • 该约束表示:传入的类型必须具有 length 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface ILength {
length: number
}

// Type extends ILength 添加泛型约束
// 表示传入的类型必须满足 ILength 接口的要求才行,也就是得有一个 number 类型的 length 属性
function id<Type extends ILength>(value: Type): Type {
console.log(value.length)
return value
}

id('abc')
id(['a', 'b', 'c'])
id({ length: 8 })
1
2
3
4
// T 也可以继承字面量类型
function id<T extends { length: number }>(value: T): number {
return value.length
}

多个类型变量

泛型的类型变量可以有多个,并且类型变量之间还可以约束(比如第二个类型变量受第一个类型变量约束)。

📝 需求:创建一个函数来获取对象中属性的值。

1
2
3
4
5
function getProp<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key]
}
let person = { name: 'jack', age: 18 }
getProp(person, 'name')
  1. 添加了第二个类型变量 Key,两个类型变量之间使用 , 逗号分隔。

  2. keyof 关键字接收一个对象类型,生成其键名称的联合类型,例如这里也就是:'name' | 'age'

  3. 类型变量 Key 受 Type 约束,即 Key 只能是 Type 所有键中的任意一个,或者说只能访问对象中存在的属性。

🤔 思考下面写法。

1
2
3
4
5
6
7
function getProp<Type, Key extends keyof { name: string; age: number }>(obj: Type, key: Key) {
// Type 'Key' cannot be used to index type 'Type'.
// 原因:因为 Type 是泛型,什么类型都有可能,而 'name' | 'age' 并没有和 Type 产生关系
return obj[key]
}
let person = { name: 'jack', age: 18 }
getProp(person, 'name')

了解:也可以对 Type 进行约束。

1
2
3
4
5
// Type extends object 表示:Type 应该是一个对象类型,如果不是对象类型,就会报错
// 注意:如果要用到对象类型,应该用 object ,而不是 Object
function getProperty<Type extends object, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key]
}

泛型接口

接口也可以配合泛型来使用,以增加其灵活性,增强其复用性。

1
2
3
4
5
6
7
8
interface User<T> {
name: T
age: number
}
const user: User<string> = {
name: 'ifer',
age: 18,
}

思考下面代码的意思,并写出对应的实现。

1
2
3
4
interface IdFunc<Type> {
id: (value: Type) => Type // 接收什么类型,返回什么类型
ids: () => Type[] // 返回值是,根据接收到的类型组成的数组
}
1
2
3
4
5
6
7
8
let obj: IdFunc<number> = {
id(value) {
return value
},
ids() {
return [1, 3, 5]
},
}
  1. 在接口名称的后面添加 <类型变量>,那么,这个接口就变成了泛型接口。

  2. 接口的类型变量,对接口中所有其他成员可见,也就是接口中所有成员都可以使用类型变量

  3. 使用泛型接口时,需要显式指定具体的类型(比如,此处的 IdFunc<number>)。

  4. 此时,id 方法的参数和返回值类型都是 number,ids 方法的返回值类型是 number[]

1
2
// 这其实也是通过泛型接口的形式来定义的数组类型
const arr: Array<number> = [1, 2, 3]
1
2
3
4
5
6
// 模拟实现
interface IArray<T> {
[key: number]: T
}

const arr: IArray<string> = ['a', 'b']

泛型工具类型

  • 泛型工具类型:TS 内置了一些常用的工具类型,来简化 TS 中的一些常见操作。

  • 说明:它们都是基于泛型实现并且是内置的,可以直接在代码中使用,这些工具类型有很多,主要学习以下几个。

    a,Partial<Type>

    b,Readonly<Type>

    c,Pick<Type, Keys>

Partial

  • Partial 用来构造(创建)一个类型,将 Type 的所有属性设置为可选。
1
2
3
4
5
6
7
type Props = {
id: string
children: number[]
}

// 构造出来的新类型 PartialProps 结构和 Props 相同,但所有属性都变为可选的啦
type PartialProps = Partial<Props>

了解 Partial 实现原理。

1
2
3
4
5
// keyof 获取类,对象,接口的所有属性名组成的联合类型
// in 表示遍历,一般用于联合类型
type MyPartial<T> = {
[P in keyof T]?: T[P]
}

Readonly

  • Readonly 用来构造一个类型,将 Type 的所有属性都设置为 readonly(只读)。

  • 当我们想给 id 属性重新赋值时,就会报错:无法分配到 “id”,因为它是只读属性。

1
2
3
4
5
6
7
8
9
type Props = {
id: string
children: number[]
}
// 构造出来的新类型 ReadonlyProps 结构和 Props 相同,但所有属性都变为只读的啦
type ReadonlyProps = Readonly<Props>

let props: ReadonlyProps = { id: '1', children: [] }
props.id = '2' // Cannot assign to 'id' because it is a read-only property

Pick

  • Pick<Type, Keys> 从 Type 中选择一组属性来构造新类型。

  • Pick 工具类型有两个类型变量,1. 表示选择谁的属性,2. 表示选择哪几个属性。

  • 第二个类型变量传入的属性只能是第一个类型变量中存在的属性。

  • 构造出来的新类型 PickProps,只有 id 和 title 两个属性类型。

1
2
3
4
5
6
7
interface Props {
id: string
title: string
children: number[]
}
// 摘出 id 和 title
type PickProps = Pick<Props, 'id' | 'title'>

Omit,和 Pick 相反,表示排除的意思。

1
2
// 排除 id 和 title
type OmitProps = Omit<Props, 'id' | 'title'>

TypeScript 与 Vue

参考链接,Vue3 配合 TS,需要额外安装一个 VSCode 插件:TypeScript Vue Plugin (Volar)。

image-20220216233612366

defineProps

目标:掌握 defineProps 如何配合 TS 使用。

  1. defineProps 配合 Vue 默认语法进行类型校验。

App.vue

1
2
3
4
5
6
7
8
9
<script setup>
import Child from './Child.vue'
</script>
<template>
<section>
<h3>App</h3>
<Child :money="100" car="奥托" />
</section>
</template>

Child.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup>
defineProps({
money: {
type: Number,
requied: true,
},
car: {
type: String,
required: true,
},
})
</script>
<template>
<div>
<p>money: {{ money }}</p>
<p>car: {{ car }}</p>
</div>
</template>
  1. 通过 defineProps 的泛型参数来定义 props 的类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 记得指定 lang="ts" -->
<script setup lang="ts">
defineProps<{
money: number
car?: string
}>()
</script>
<template>
<div>
<p>money: {{ money }}</p>
<p>car: {{ car }}</p>
</div>
</template>

也可以把类型定义提取为 interface。

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup lang="ts">
interface IProps {
money: number
car?: string
}
defineProps<IProps>()
</script>
<template>
<div>
<p>money: {{ money }}</p>
<p>car: {{ car }}</p>
</div>
</template>
  1. 泛型的方式,如何指定默认值呢?
1
2
3
4
5
6
7
<script lang="ts" setup>
// 使用ts的泛型指令props类型
const { money, car = '小黄车' } = defineProps<{
money: number
car?: string
}>()
</script>

🤔 如果提供的默认值需要在模板中渲染,需要额外添加配置vite.config.js

1
2
3
4
5
6
7
8
9
10
11
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue({
reactivityTransform: true,
}),
],
})

defineEmits

目标:掌握 defineEmit 如何配合 TS 使用。

  1. 自定义事件,App.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'
const money = ref(100)
const car = ref('奥托')
const changeMoney = (content: number) => {
money.value = content
}
const changeCar = (content: string) => {
car.value = content
}
</script>
<template>
<section>
<h3>App</h3>
<!-- #1 -->
<Child :money="money" :car="car" @changeMoney="changeMoney" @changeCar="changeCar" />
</section>
</template>
  1. defineEmits 生成 emits 触发,Child.vue

Child.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup lang="ts">
const { money, car = '小黄车' } = defineProps<{
money: number
car?: string
}>()
// #2
const emits = defineEmits(['changeMoney', 'changeCar'])
</script>
<template>
<div>
<p>money: {{ money }}</p>
<p>car: {{ car }}</p>
<button @click="emits('changeMoney', 10000)">change money</button>
<button @click="emits('changeCar', '奔驰')">change car</button>
</div>
</template>

defineEmits 配合 TS 使用。

1
2
3
4
const emits = defineEmits<{
(e: 'changeMoney', money: number): void
(e: 'changeCar', car: string): void
}>()

ref 函数

目标:掌握 ref 配合 TS 如何使用。

  1. 通过泛型指定 value 的值类型,如果是简单值,该类型可以省略。
1
2
const money = ref<number>(10)
const money = ref(10)
  1. 如果是复杂类型,可以通过泛型来配合 type 或 interface 来指定初始值的类型。

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!-- 不要忘了 lang="ts" -->
<script setup lang="ts">
import { ref } from 'vue'

/* type Todo = {
id: number
name: string
done: boolean
} */
interface Todo {
id: number
name: string
done: boolean
}

const list = ref<Todo[]>([])

setTimeout(() => {
list.value = [
{ id: 1, name: '吃饭', done: false },
{ id: 2, name: '睡觉', done: true },
]
})
</script>
<template>
<ul>
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
</ul>
</template>
  1. 通过 Vue 提供的 Ref 泛型工具直接给声明的变量添加类型(了解即可)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- 不要忘了 lang="ts" -->
<script setup lang="ts">
import { Ref, ref } from 'vue'

interface Todo {
id: number
name: string
done: boolean
}

// mark
const list: Ref<Todo[]> = ref([])

setTimeout(() => {
list.value = [
{ id: 1, name: '吃饭', done: false },
{ id: 2, name: '睡觉', done: true },
]
})
</script>
<template>
<ul>
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
</ul>
</template>

reactive

  1. 直接给变量添加类型(推荐)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup lang="ts">
import { reactive } from 'vue'

interface IPerson {
name: string
age: number | string
}
const p: IPerson = reactive({
name: 'ifer',
age: 18,
})
</script>

<template>
<div>
<p>name: {{ p.name }}</p>
<p>age: {{ p.age }}</p>
</div>
</template>
  1. 通过泛型参数的形式增加类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup lang="ts">
import { reactive } from 'vue'

interface IPerson {
name: string
age: number | string
}
const p = reactive<IPerson>({
name: 'ifer',
age: 18,
})
</script>

<template>
<div>
<p>name: {{ p.name }}</p>
<p>age: {{ p.age }}</p>
</div>
</template>

computed

通过泛型可以指定 computed 计算属性的类型,通常可以省略(利用 TS 类型推导的能力)。

1
2
3
4
const leftCount = computed<number>(() => {
return list.value.filter((item) => item.done).length
})
console.log(leftCount.value)

事件处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup lang="ts">
import { ref } from 'vue'

const mouse = ref({
x: 0,
y: 0,
})
const move = (e: MouseEvent) => {
mouse.value.x = e.pageX
mouse.value.y = e.pageY
}
</script>
<template>
<p>x: {{ mouse.x }}</p>
<p>y: {{ mouse.y }}</p>
<h1 @mousemove="move($event)">Hello</h1>
</template>

ref 属性

ref 操作 DOM

1
2
3
4
5
6
7
8
9
10
11
<script setup lang="ts">
import { onMounted, ref } from 'vue'
// const imgRef = ref<HTMLImageElement>()
const imgRef = ref<HTMLImageElement | null>(null)
onMounted(() => {
console.log(imgRef.value?.src)
})
</script>
<template>
<img src="https://pinia.vuejs.org/logo.svg" ref="imgRef" />
</template>

如何查看一个 DOM 对象的类型:通过控制台进行查看。

1
document.createElement('img').__proto__

ref 操作组件

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'
// 通过内置的泛型工具 InstanceType 可以获取构造函数类型的实例类型
const childCmp = ref<InstanceType<typeof Child> | null>(null)

const handleClick = () => {
childCmp.value?.logHello()
}
</script>
<template>
<section>
<h3>App</h3>
<button @click="handleClick">click</button>
<hr />
<Child ref="childCmp" />
</section>
</template>

Child.vue

1
2
3
4
5
6
7
8
9
10
11
<script setup lang="ts">
const logHello = () => {
console.log('🤣')
}
defineExpose({
logHello,
})
</script>
<template>
<div>Child</div>
</template>

可选链操作符

目标:掌握 JS 中的提供的可选链操作符语法。

可选链操作符( ?. )允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效,参考文档

1
2
3
4
const nestedProp = obj.first?.second
// 等价于
let temp = obj.first
let nestedProp = temp === null || temp === undefined ? undefined : temp.second
1
2
3
4
5
6
7
// 旧写法
if (obj.fn) {
obj.fn()
}
obj.fn && obj.fn()
// 可选链
obj.fn?.()

非空断言

目标:掌握 TS 中的非空断言的使用语法。

  • 如果我们明确的知道对象的属性一定不会为空,那么可以使用非空断言 !
1
2
3
4
5
// 告诉 TS, 明确的指定 obj 不可能为空
const nestedProp = obj!.second

// 表示 document.querySelector('div') 不可能为空
console.log(document.querySelector('div')!.innerHTML)
  • 注意:非空断言一定要确保有该属性才能使用,不然使用非空断言会导致 Bug。

TS 类型声明文件

基本介绍

今天几乎所有的 JavaScript 应用都会引入许多第三方库来完成任务需求,这些第三方库不管是否是用 TS 编写的,最终都要编译成 JS 代码,才能发布给开发者使用。

我们知道是 TS 提供了类型,才有了代码提示和类型保护等机制,但在项目开发中使用第三方库时,你会发现它们几乎都有相应的 TS 类型,这些类型是怎么来的呢?

答案:类型声明文件:用来为已存在的 JS 库提供类型信息。

TS 中有如下两种文件类型。

  • .ts 文件。

    • 既包含类型信息又可执行代码,可以被编译为 .js 文件,然后,执行代码。

    • 用途:编写程序代码的地方。

  • .d.ts 文件。

    • 只包含类型信息的类型声明文件,专门为 JS 提供类型信息。

    • 类型声明文件不会生成 .js 文件,仅用于提供类型信息,在 .d.ts 文件中不允许出现可执行的 JS 代码,只用于提供类型。

总结:.ts 是 implementation(代码实现文件);.d.ts 是 declaration(类型声明文件),如果要为已有的 JS 库提供类型信息,可以使用 .d.ts 文件。

内置类型声明文件

  • TS 为 JS 中所有的标准化内置 API 都提供了声明文件。

  • 比如,在使用数组时,数组所有方法都会有相应的代码提示以及类型信息。

1
2
3
const strs = ['a', 'b', 'c']
// 鼠标放在 forEach 上查看类型
strs.forEach
  • 实际上这都是 TS 提供的内置类型声明文件。

  • 可以通过 Ctrl + 鼠标左键(Mac:Command + 鼠标左键)来查看内置类型声明文件内容。

  • 比如,查看 forEach 方法的类型声明,在 VSCode 中会自动跳转到 lib.es5.d.ts 类型声明文件中。

  • 当然,像 window、document 等 BOM、DOM API 也都有相应的类型声明(lib.dom.d.ts)。

第三方库类型声明文件

目前,几乎所有常用的第三方库都有相应的类型声明文件,第三方库的类型声明文件有两种存在形式。

  • 库自带类型声明文件。

    • 比如 axios,通过查看 node_modules/axios 目录可以看到。

    • 这种情况下,正常导入该库,TS 就会自动加载库自己的类型声明文件,以提供该库的类型声明。

  • 由 DefinitelyTyped 提供。

    • DefinitelyTyped 是一个 Github 仓库,用来提供高质量 TypeScript 类型声明。

    • DefinitelyTyped 链接

    • 可以通过 npm/yarn 来下载该仓库提供的 TS 类型声明包,这些包的名称格式为:@types/*

    • 比如,@types/react、@types/lodash 等。

    • 说明:在实际项目开发时,如果你使用的第三方库没有自带的声明文件,VSCode 会给出明确的提示。

    1
    2
    3
    import _ from 'lodash'

    // 在 VSCode 中,查看 'lodash' 前面的提示
    • 解释:当安装 @types/* 类型声明包后,TS 也会自动加载该类声明包,以提供该库的类型声明。

    • 补充:TS 官方文档提供了一个页面,可以来查询 @types/* 库

自定义类型声明文件

  • 如果多个 .ts 文件中都用到同一个类型,此时可以创建 .d.ts 文件提供该类型,实现类型共享。

  • 为已有 JS 文件提供类型声明。

    1. 创建 index.d.ts 类型声明文件。

    2. 创建需要共享的类型,并使用 export 导出(TS 中的类型也可以使用 import/export 实现模块化功能)。

    3. 在需要使用共享类型的 .ts 文件中,通过 import 导入即可(.d.ts 后缀导入时,直接省略)。

  • 类型声明文件的使用说明。

    • 说明:TS 项目中也可以使用 .js 文件,在导入 .js 文件时,TS 会自动加载与 .js 同名的 .d.ts 文件,以提供类型声明。

    • declare 关键字,用于类型声明,为 .js 文件中已存在的变量声明类型,而不是创建一个新的变量。

      • 对于 type、interface 等这些明确就是 TS 类型的(只能在 TS 中使用的),可以省略 declare 关键字。

      • 对于 let、function 等具有双重含义(在 JS、TS 中都能用),应该使用 declare 关键字,明确指定此处用于类型声明。

utils/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const count = 10
const songName = '痴心绝对'
const position = {
x: 0,
y: 0,
}

function add(x, y) {
return x + y
}

function changeDirection(direction) {
console.log(direction)
}

const fomartPoint = (point) => {
console.log('当前坐标:', point)
}

export { count, songName, position, add, changeDirection, fomartPoint }

定义类型声明文件,utils/index.d.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
declare let count: number

declare let songName: string

interface Position {
x: number
y: number
}

declare let position: Position

declare function add(x: number, y: number): number

type Direction = 'left' | 'right' | 'top' | 'bottom'

declare function changeDirection(direction: Direction): void

type FomartPoint = (point: Position) => void

declare const fomartPoint: FomartPoint

export { count, songName, position, add, changeDirection, FomartPoint, fomartPoint }

综合练习

Axios 与 TypeScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script setup lang="ts">
import Channel from './components/Channel.vue'
import NewsList from './components/NewsList.vue'
import axios from 'axios'
import { ref } from 'vue'

type ApiResponse<T> = {
message: string
data: T
}

type ChannelItem = { id: number; name: string }

const list = ref<ChannelItem[]>([])

async function getList() {
const { data } = await axios.get<ApiResponse<{ channels: ChannelItem[] }>>('http://geek.itheima.net/v1_0/channels')
list.value = data.data.channels
}
getList()
</script>

image-20220217222351298

静态结构

引入通用样式(资料中已经准备好)。

main.ts

1
import './styles/index.css'

styles/index.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
body {
margin: 0;
padding: 0;
}
*,
*:before,
*:after {
box-sizing: inherit;
}

li {
list-style: none;
}
dl,
dd,
dt,
ul,
li {
margin: 0;
padding: 0;
}

.no-padding {
padding: 0px !important;
}

.padding-content {
padding: 4px 0;
}

a:focus,
a:active {
outline: none;
}

a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
}

b {
font-weight: normal;
}

div:focus {
outline: none;
}

.fr {
float: right;
}

.fl {
float: left;
}

.pr-5 {
padding-right: 5px;
}

.pl-5 {
padding-left: 5px;
}

.block {
display: block;
}

.pointer {
cursor: pointer;
}

.inlineBlock {
display: block;
}
.catagtory {
display: flex;
overflow: hidden;
overflow-x: scroll;
background-color: #f4f5f6;
width: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 999;
}
.catagtory li {
padding: 0 15px;
text-align: center;
line-height: 40px;
color: #505050;
cursor: pointer;
z-index: 99;
white-space: nowrap;
}
.catagtory li.select {
color: #f85959;
}
.list {
margin-top: 60px;
}
.article_item {
padding: 0 10px;
}
.article_item .img_box {
display: flex;
justify-content: space-between;
}
.article_item .img_box .w33 {
width: 33%;
height: 90px;
display: inline-block;
}
.article_item .img_box .w100 {
width: 100%;
height: 180px;
display: inline-block;
}
.article_item h3 {
font-weight: normal;
line-height: 2;
}
.article_item .info_box {
color: #999;
line-height: 2;
position: relative;
font-size: 12px;
}
.article_item .info_box span {
padding-right: 10px;
}
.article_item .info_box span.close {
border: 1px solid #ddd;
border-radius: 2px;
line-height: 15px;
height: 12px;
width: 16px;
text-align: center;
padding-right: 0;
font-size: 8px;
position: absolute;
right: 0;
top: 7px;
}

components/Channel.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<script lang="ts" setup></script>

<template>
<ul class="catagtory">
<li class="select">开发者资讯</li>
<li>ios</li>
<li>c++</li>
<li>android</li>
<li>css</li>
<li>数据库</li>
<li>区块链</li>
<li>go</li>
<li>产品</li>
<li>后端</li>
<li>linux</li>
<li>人工智能</li>
<li>php</li>
<li>javascript</li>
<li>架构</li>
<li>前端</li>
<li>python</li>
<li>java</li>
<li>算法</li>
<li>面试</li>
<li>科技动态</li>
<li>js</li>
<li>设计</li>
<li>数码产品</li>
<li>html</li>
<li>软件测试</li>
<li>测试开发</li>
</ul>
</template>

<style scoed lang="less"></style>

components/NewsList.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script lang="ts" setup></script>

<template>
<div className="list">
<div className="article_item">
<h3 className="van-ellipsis">python数据预处理 :数据标准化</h3>
<div className="img_box">
<img src="http://geek.itheima.net/resources/images/11.jpg" className="w100" alt="" />
</div>
<div className="info_box">
<span>13552285417</span>
<span>0评论</span>
<span>2018-11-29T17:02:09</span>
</div>
</div>
</div>
</template>

<style scoed lang="less"></style>

App.vue

1
2
3
4
5
6
7
8
9
<script setup lang="ts">
import Channel from './components/Channel.vue'
import NewsList from './components/NewsList.vue'
</script>

<template>
<Channel />
<NewsList />
</template>

接口说明

获取频道列表:http://geek.itheima.net/v1_0/channels。

获取频道新闻:http://geek.itheima.net/v1_0/articles?channel_id=频道id&timestamp=时间戳。

Pinia 环境搭建

  1. 配置 pinia,main.ts
1
2
3
4
5
6
import { createApp } from 'vue'
import './styles/index.css'
import App from './App.vue'
import { createPinia } from 'pinia'
const pinia = createPinia()
createApp(App).use(pinia).mount('#app')
  1. 创建 channel 模块,store/modules/channel.ts
1
2
3
4
5
6
7
8
9
10
11
import { defineStore } from 'pinia'

const useChannelStore = defineStore('channel', {
state() {
return {}
},
getters: {},
actions: {},
})

export default useChannelStore
  1. 创建根 store 并关联 channel 模块,store/index.ts
1
2
3
4
5
6
7
import useChannelStore from './modules/channel'

export default function useStore() {
return {
channel: useChannelStore(),
}
}

获取频道

  1. 创建频道列表类型和 ApiResponse,types/data.d.ts
1
2
3
4
5
6
7
8
9
export type ChannelList = {
id: number
name: string
}[]

export type ApiResponse<T> = {
message: string
data: T
}
  1. 定义请求数据的 action 并存储到 state,store/modules/channel.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import axios from 'axios'
import { defineStore } from 'pinia'
import { ApiResponse, ChannelList } from '../../types/data'

const useChannelStore = defineStore('channel', {
state() {
return {
list: [] as ChannelList,
}
},
actions: {
async getList() {
const { data } = await axios.get<ApiResponse<{ channels: ChannelList }>>('http://geek.itheima.net/v1_0/channels')
this.list = data.data.channels
},
},
})

export default useChannelStore
  1. 视图中触发 action,components/Channel.vue
1
2
3
4
5
6
7
8
9
10
11
<script lang="ts" setup>
import useStore from '../store'
const { channel } = useStore()
channel.getList()
</script>

<template>
<ul class="catagtory">
<li class="select" v-for="item in channel.list" :key="item.id">{{ item.name }}</li>
</ul>
</template>

频道高亮效果

  1. 定义 active state 和 changeActive action,store/modules/channel.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import axios from 'axios'
import { defineStore } from 'pinia'
import { ApiResponse, ChannelList } from '../../types/data'

const useChannelStore = defineStore('channel', {
state() {
return {
list: [] as ChannelList,
active: -1, // #1
}
},
actions: {
async getList() {
const {
data: {
data: { channels },
},
} = await axios.get<ApiResponse<{ channels: ChannelList }>>('http://geek.itheima.net/v1_0/channels')
this.list = channels
// #2
this.active = channels[0].id
},
changeActive(id: number) {
this.active = id
},
},
})

export default useChannelStore
  1. 绑定 class 和点击高亮,components/Channel.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script lang="ts" setup>
import useStore from '../store'
const { channel } = useStore()
channel.getList()
</script>

<template>
<ul class="catagtory">
<li
:class="{
select: item.id === channel.active,
}"
v-for="item in channel.list"
:key="item.id"
@click="channel.changeActive(item.id)"
>
{{ item.name }}
</li>
</ul>
</template>

根据频道 ID 获取文章

  1. 定义文章列表类型,types/data.d.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export type ChannelList = {
id: number
name: string
}[]

export type ApiResponse<T> = {
message: string
data: T
}

export type ArticleList = {
art_id: string
aut_id: string
aut_name: string
comm_count: number
cover: {
type: number
images?: string[]
}
is_top: number
pubdate: string
title: string
}[]
  1. 定义 news 模块,store/modules/news.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { defineStore } from 'pinia'
import { ApiResponse, ArticleList } from '../../types/data'
import axios from 'axios'

const useNewsStore = defineStore('news', {
state() {
return {
articleList: [] as ArticleList,
}
},
actions: {
async getArticleList(id: number) {
const { data } = await axios.get<ApiResponse<{ pre_timestamp: string; results: ArticleList }>>(`http://geek.itheima.net/v1_0/articles?channel_id=${id}&timestamp=${Date.now()}`)
this.articleList = data.data.results
},
},
})

export default useNewsStore
  1. 关联 new 模块到跟模块,store/index.ts
1
2
3
4
5
6
7
8
9
import useChannelStore from './modules/channel'
import useNewsStore from './modules/news'

export default function useStore() {
return {
channel: useChannelStore(),
news: useNewsStore(),
}
}
  1. 监听 channel.active,发起请求并渲染,components/NewsList.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script lang="ts" setup>
import { watch } from 'vue'
import useStore from '../store'
const { news, channel } = useStore()
watch(
() => channel.active,
() => {
news.getArticleList(channel.active)
}
)
</script>

<template>
<div className="list">
<div className="article_item" v-for="item in news.articleList" :key="item.art_id">
<h3 className="van-ellipsis">{{ item.title }}</h3>
<div className="img_box">
<img :src="item.cover.images ? item.cover.images[0] : 'http://geek.itheima.net/resources/images/11.jpg'" className="w100" alt="" />
</div>
<div className="info_box">
<span>{{ item.aut_name }}</span>
<span>{{ item.comm_count }}评论</span>
<span>{{ item.pubdate }}</span>
</div>
</div>
</div>
</template>