TypeScript 使用教程(很细节)

866 阅读25分钟

学习目标

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

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

TypeScript 概述

概念

  • TypeScript 是微软开发的编程语言,它是 JavaScript 的超集,可以在任何运行 JavaScript 的地方运行,官方文档中文文档,不再维护
  • TypeScript = Type + JavaScript(在 JS 基础之上,为 JS 添加了类型支持/类型检测)。
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)。

// 使用 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 代码

  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 文件,代码中就没有类型信息了。
# 监听 index.ts 文件的变化并编译
tsc -w index.ts  # 窗口 1
# 运行编译后的代码
nodemon index.js # 窗口 2

TypeScript 基础

类型注解

目标

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

内容

  • TypeScript 是 JS 的超集,TS 提供了 JS 的所有功能,并且额外的增加了:类型系统,JS 虽然也有类型(比如,number/string 等),但 JS 并不会对类型进行校验和提示。
  • TypeScript 类型系统的主要优势:校验和提示
let age: number = 18
  • 说明:代码中的 :number 就是类型注解
  • 作用:为变量添加类型约束,比如上述代码中,约定变量 age 的类型为 number 类型。
  • 解释:约定了什么类型,就只能给变量赋值该类型的值,也会出现该类型相关的提示
// 错误原因:将 string 类型的值赋值给了 number 类型的变量,类型不一致
let age: number = '18'

原始类型

目标

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

内容

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

  • JS 已有类型。
// 原始类型:`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
let numbers: number[] = [1, 3, 5]
// 写法 2
let strings: Array<string> = ['a', 'b', 'c']
strings.push('d') // 后续 push 的数据也必须是字符串

联合类型

目标

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

内容

  • 需求:数组中既有 number 类型,又有 string 类型,这个数组的类型应该如何写?
// 定义一个数组,数组中可以有数字或者字符串, 需要注意 | 的优先级
let arr: (number | string)[] = [1, 'abc', 2]
  • 解释:|(竖线)在 TS 中叫做联合类型,即由两个或多个其他类型组成的类型,表示可以是这些类型中的一种。
  • 注意:这是 TS 中联合类型的语法,只有一根竖线,不要与 JS 中的或(||)混淆了。
  • 场景:定时器的初始变量定义。
// 有问题的代码
let timer = null
// Type 'number' is not assignable to type 'null'.
timer = setInterval(() => {})
// 解决,思考除了下面方法还有其他办法吗?
let timer: number | null = null
timer = setInterval(() => {})
// 忽略
// 通过 tsc --init 命令可以生成配置文件
// 通过 strictNullChecks 指定为 true 可以开启对 null 和 undefined 的检测
// 即便开启了检测,当 null 赋值给某个变量时,这个变量会被推断为 any 类型
// !通过 noImplicitAny 指定为 false 可以禁用 any 类型,此时 null 赋值给某个变量时将会是 null 类型
let timer: number | null = null
timer = setInterval(() => {}, 1000)

类型别名

目标

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

内容

  • 类型别名作用:为任意类型起别名,别名甚至可以是中文。
type s = string
const myName: s = 'ifer'type 字符串类型 = string
const myAddress: 字符串类型 = '河南老乡~'
  • 使用场景:当同一类型(复杂)且可能被多次使用时,可以通过类型别名,简化该类型的使用
type CustomArray = (number | string)[]
​
let arr1: CustomArray = [1, 'a', 3, 'b']
let arr2: CustomArray = ['x', 'y', 6, 7]
  • 解释说明。

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

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

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

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

函数类型

基本使用

目标

能够给函数指定类型。

内容

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

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

void 类型

目标

能够了解 void 类型的使用。

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

可选参数

目标

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

内容
  • 使用函数实现某个功能时,参数可以传也可以不传,这种情况下,在给函数参数指定类型时,就用到可选参数了。
  • 比如,数组的 slice 方法,可以 slice() 也可以 slice(1) 还可以 slice(1, 3)
  • 可选参数语法:在可传可不传的参数名称后面添加 ?(问号)。
// startend 可传可不传,传就传 number 类型
function mySlice(start?: number, end?: number): void {
    console.log('起始索引:', start, '结束索引:', end)
}
  • 注意:可选参数只能出现在参数列表的最后,也就是说可选参数后面不能再出现必选参数。

参数默认值

目标

能够给函数指定默认值。

内容

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

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

对象类型

基本使用

目标

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

内容

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

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

对象可选属性

  • 对象的属性或方法,也可以是可选的,此时就用到可选属性了。
  • 比如,我们在使用 axios({ ... }) 时,如果发送 GET 请求,method 属性就可以省略。
  • 可选属性的语法与函数可选参数的语法一致,都使用 ? 来表示。
type Config = {
    url: string
    method?: string
}
​
function myAxios(config: Config) {
    console.log(config)
}

练习

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

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 开头。
  • 声明接口后,直接使用接口名称作为变量的类型。
  • 因为每一行只有一个属性类型,因此,属性类型后没有 ;(分号)。
interface IStudent {
    name: string
    gender: string
    study(): void
}
​
const stu: IStudent = {
    name: 'xxx',
    gender: 'man',
    study() {
        console.log('学学学')
    },
}

接口继承

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

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

interface vs type

相同点
  • 都可以描述对象或者函数。
// interface 描述对象
interface IPerson {
    name: string
    age: number
}
const p: IPerson = { name: 'ifer', age: 18 }
// interface 描述函数
interface ISetPerson {
    (name: string, age: number): void
}
const setPerson: ISetPerson = (name, age) => {}
​
setPerson('ifer', 18)
// type 描述对象
type TPerson = {
    name: string
    age: number
}
const p: TPerson = { name: 'ifer', age: 18 }
// type 描述函数
type TSetPerson = {
    (name: string, age: number): void
}
const setPerson: TSetPerson = (name, age) => {}
​
setPerson('ifer', 18)
  • 都允许拓展,语法不一样。
// interface extends interface
interface IName {
    name: string
}
interface IPerson extends IName {
    age: number
}
​
const p: IPerson = {
    name: 'ifer',
    age: 18,
}
// interface extends type
type TName = { name: string }
interface IPerson extends TName {
    age: number
}
​
const p: IPerson = {
    name: 'ifer',
    age: 18,
}
// type & type
type TName = { name: string }
type TPerson = { age: number } & TNameconst p: TPerson = {
    name: 'ifer',
    age: 18,
}
// type & interface
interface IName {
    name: string
}
type TPerson = { age: number } & INameconst p: TPerson = {
    name: 'ifer',
    age: 18,
}
不同点

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

type NumStr = number | string

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

interface IPerson {
    name: string
}
interface IPerson {
    age: number
}
const p: IPerson = {
    name: 'ifer',
    age: 18,
}

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

元组类型

  • 使用 number[] 的缺点:不严谨,因为该类型的数组中可以出现任意多个数字。
  • 元组 Tuple,元组是特殊的数组类型,它能确定元素的个数以及特定索引对应的类型
const position: [number, number] = [39.5427, 116.2317]
  • 解释说明。

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

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

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

类型推论

  • 在 TS 中,某些没有明确指出类型的地方,TS 的类型推论机制会帮助提供类型
  • 换句话说:由于类型推论的存在,这些地方,类型注解可以省略不写。
  • 常见的发生类型推论的 2 种场景:声明变量并初始化时;决定函数返回值时。
// 变量 age 的类型被自动推断为:number
let age = 18const obj = {
    name: 'ifer',
    age: 18,
    show() {},
}
​
// 函数返回值的类型被自动推断为:number
function add(num1: number, num2: number) {
    return num1 + num2
}
  • 推荐:代码写熟了之后,有类型推论的情况下可以省略类型注解,充分利用 TS 类型推论的能力,提升开发效率。
  • 技巧:如果不知道类型,可以通过鼠标放在变量名称上,利用 VSCode 的提示来查看类型。
  • 建议:在 VSCode 中写代码的时候,多看方法、属性的类型,养成写代码看类型的习惯,例如 const oDiv = document.createElement('div')

字面量类型

基本使用

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

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() {} 等。

使用方式和场景

  • 使用方式:字面量类型常配合联合类型一起使用
  • 使用场景:用来表示一组明确的可选值列表,比如在贪吃蛇游戏中,游戏方向的值只能是上、下、左、右中的一个。
type Direction = 'up' | 'down' | 'left' | 'right'
function changeDirection(direction: Direction) {
    console.log(direction)
}
changeDirection('up') // 调用函数时,会有类型提示
  • 解释:参数 direction 的值只能是 up/down/left/right 中的任意一个。
  • 优势:相比于 string 类型,使用字面量类型更加精确、严谨
  • 其他应用场景,性别和 Redux 中的 Action 等等。
type Gender = '男' | '女'
const zs: Gender = '男'
type Action = {
    type: 'TODO_ADD' | 'TODO_DEL' | 'TODO_CHANGE' | 'TODO_FIND'
}
​
function reducer(state, action: Action) {
    switch (action.type) {
        case 'TODO_ADD': // 这里会自动具有提示
    }
}

枚举类型

基本使用

  • 枚举的功能类似于字面量类型+联合类型组合的功能,也可以表示一组明确的可选值。
  • 枚举:定义一组命名常量,它描述一个值,该值可以是这些命名常量中的一个。
  • 使用 enum 关键字定义枚举,约定枚举名称以大写字母开头。
  • 枚举中的多个值之间通过 ,(逗号)分隔,定义好枚举后,直接使用枚举名称作为类型注解。
// 创建枚举
enum Direction {
    Up,
    Down,
    Left,
    Right,
}
​
// 可以当做类型使用枚举
function changeDirection(direction: Direction) {
    console.log(direction)
}
​
// 也可以当做值使用枚举
// 调用函数时,需要传入:枚举 Direction 成员的任意一个,类似于 JS 中的对象,直接通过点(.)语法 访问枚举的成员
changeDirection(Direction.Up)

数字枚举

  • 问题:我们把枚举成员作为了函数的实参,它的值是什么呢?
  • 解释:通过将鼠标移入 Direction.Up,可以看到枚举成员 Up 的值为 0。
  • 注意:枚举成员是有值的,默认为:从 0 开始自增的数值。
  • 我们把枚举成员的值为数字的枚举称为:数字枚举
  • 当然,也可以通过“等号”给枚举中的成员指定初始值,如下所示。
// Down -> 11Left -> 12Right -> 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 代码
  • 说明:枚举与前面讲到的字面量类型 + 联合类型组合的功能类似,都用来表示一组明确的可选值列表。
  • 推荐:字面量类型 + 联合类型组合的方式,因为相比枚举,这种方式更加直观、简洁、高效。
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 = {}))

字符串枚举

  • 定义:枚举成员的值是字符串称为字符串枚举。
  • 注意:字符串枚举没有自增长行为,因此,字符串枚举的每个成员必须有初始值
enum Direction {
    Up = 'UP',
    Down = 'DOWN',
    Left = 'LEFT',
    Right = 'RIGHT',
}

🧐 具体的使用案例。

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 标签。

// 注意 document.querySelector('a') 这种写法会自动推断出是 HTMLLinkElement 类型
const oLink = document.getElementById('link')
  • 注意:该方法返回的类型是 HTMLElement,该类型只包含所有标签公共的属性或方法,不包含 a 标签特有的 href 等属性,这个类型太宽泛(不具体),无法操作 href 等 a 标签特有的属性或方法。
  • 解决方式:这种情况下就需要使用类型断言指定更加具体的类型。
const oLink = document.getElementById('link') as HTMLAnchorElement
  • 解释说明。

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

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

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

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

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

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

typeof

  • JS 中的 typeof 可以在运行时判断类型,TS 中的 typeof 可以在编译时获取类型。
interface Person {
    name: string
    age: number
}
const person: Person = { name: 'ifer', age: 18 }
​
// 获取 person 的类型,得到的就是 Person 接口类型
type p = typeof person
  • TS 中 typeof 的使用场景:根据已有变量的值,获取该值的类型,来简化类型书写。
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)、类等的所有属性名组成的联合类型。

// 接口
interface Person {
    name: string
    age: number
}
type K1 = keyof Person // "name" | "age"
type K2 = keyof Person[] // "length" | "toString" | "pop" | "push" | "concat" | "join"
// 对象(要配合 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

下面的代码了解即可。

// 类
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
// 基本类型
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 时,可以对该值进行任意操作,即使可能存在错误,并且不会有代码提示。
let num: any = 8 // 任意类型,不对类型进行校验
num.toFixed() // 没有提示
num = 'xxx' // 可以赋任意值(即可以把任意值给 any 类型)
  • 尽可能的避免使用 any 类型,除非临时使用 any 来“避免”书写很长、很复杂的类型,或者有些参数就是可以使用任何类型,例如 console.log()

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

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

    b,函数参数不加类型。

unknow

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

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

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

比较

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

never

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

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

null 和 undefined

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

函数重载

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

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

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

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,使用函数重载实现。

// 一个函数可以有多个重载签名
// !重载签名:包含了函数的参数类型和返回值类型,但不包含函数体
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 函数,传入什么数据类型就返回该数据类型本身(也就是说,参数和返回值类型相同)。
function id(value: number): number {
    return value
}
  • 比如,id(10) 调用以上函数就会直接返回 10 本身,但是,该函数只接收数值类型,无法用于其他类型。
  • 为了让函数能够接受任意类型,可以将参数类型修改为 any,但是,这样就失去了 TS 的类型保护,类型不安全。
function id(value: any): any {
    return value
}

泛型函数

  • 定义。

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

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

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

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

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

function id<Type>(value: Type): Type {
    return value
}
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 函数与多种不同的类型一起工作,实现了复用的同时保证了类型安全

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

简化泛型函数调用

let num = id(10) // 省略 <number> 调用函数
let str = id('a') // 省略 <string> 调用函数
  • 在调用泛型函数时,可以省略 <类型> 来简化泛型函数的调用
  • 此时,TS 内部会采用一种叫做类型参数推断的机制,来根据传入的实参自动推断出类型变量 Type 的类型。
  • 比如,传入实参 10,TS 会自动推断出变量 num 的类型 number,并作为 Type 的类型。
  • 推荐:使用这种简化的方式调用泛型函数,使代码更简短,更易于阅读。
  • 说明:当编译器无法推断类型或者推断的类型不准确时,就需要显式地传入类型参数

泛型约束

  • 泛型函数的类型变量 Type 可以代表任意类型,这导致访问泛型类型定义的数据属性时会没有提示,或者报错。
  • 比如,id('a') 调用函数时获取参数的长度。
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 属性,因此就可以访问了。

// 其实泛型 Type 约束的是数组里面的元素
function id<Type>(value: Type[]): Type[] {
    console.log(value.length)
    return value
}
​
id<string>(['a', 'b'])

添加泛型约束

  • 创建描述约束的接口 ILength,该接口要求提供 length 属性。
  • 通过 extends 关键字使用该接口,为泛型(类型变量)添加约束。
  • 该约束表示:传入的类型必须具有 length 属性
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 })
// T 也可以继承字面量类型
function id<T extends { length: number }>(value: T): number {
    return value.length
}

多个类型变量

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

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

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 所有键中的任意一个,或者说只能访问对象中存在的属性。

🤔 思考下面写法。

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 进行约束。

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

泛型接口

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

interface User<T> {
    name: T
    age: number
}
const user: User<string> = {
    name: 'ifer',
    age: 18,
}

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

interface IdFunc<Type> {
    id: (value: Type) => Type // 接收什么类型,返回什么类型
    ids: () => Type[] // 返回值是,根据接收到的类型组成的数组
}
let obj: IdFunc<number> = {
    id(value) {
        return value
    },
    ids() {
        return [1, 3, 5]
    },
}
  1. 在接口名称的后面添加 <类型变量>,那么,这个接口就变成了泛型接口。
  2. 接口的类型变量,对接口中所有其他成员可见,也就是接口中所有成员都可以使用类型变量
  3. 使用泛型接口时,需要显式指定具体的类型(比如,此处的 IdFunc<number>)。
  4. 此时,id 方法的参数和返回值类型都是 number,ids 方法的返回值类型是 number[]
// 这其实也是通过泛型接口的形式来定义的数组类型
const arr: Array<number> = [1, 2, 3]
// 模拟实现
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 的所有属性设置为可选。
type Props = {
    id: string
    children: number[]
}
​
// 构造出来的新类型 PartialProps 结构和 Props 相同,但所有属性都变为可选的啦
type PartialProps = Partial<Props>

了解 Partial 实现原理。

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

Readonly

  • Readonly 用来构造一个类型,将 Type 的所有属性都设置为 readonly(只读)。
  • 当我们想给 id 属性重新赋值时,就会报错:无法分配到 "id",因为它是只读属性。
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 两个属性类型。
interface Props {
    id: string
    title: string
    children: number[]
}
// 摘出 id 和 title
type PickProps = Pick<Props, 'id' | 'title'>

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

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

TypeScript 与 Vue

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

defineProps

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

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

App.vue

<script setup>
    import Child from './Child.vue'
</script>
<template>
    <section>
        <h3>App</h3>
        <Child :money="100" car="奥托" />
    </section>
</template>

Child.vue

<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 配合 TS 的泛型定义 props 类型校验。
<!-- 记得指定 lang="ts" -->
<script setup lang="ts">
    defineProps<{
        money: number
        car?: string
    }>()
</script>
<template>
    <div>
        <p>money: {{ money }}</p>
        <p>car: {{ car }}</p>
    </div>
</template>
  1. props 可以通过解构来指定默认值。
<script lang="ts" setup>
    // 使用ts的泛型指令props类型
    const { money, car = '小黄车' } = defineProps<{
        money: number
        car?: string
    }>()
</script>

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

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
<script setup>
    import { ref } from 'vue'
    import Child from './Child.vue'
    const money = ref(100)
    const car = ref('奥托')
    const changeMoney = (content) => {
        money.value = content
    }
    const changeCar = (content) => {
        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

<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>

配合 TS 使用。

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

ref

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

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

App.vue

<!-- 不要忘了 lang="ts" -->
<script setup lang="ts">
    import { ref } from 'vue'
​
    type 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>

computed

通过泛型可以指定 computed 计算属性的类型,通常可以省略。

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

事件处理

<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>

Template Ref

目标:掌握 ref 操作 DOM 时如何配合 TS 使用。

<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 对象的类型:通过控制台进行查看。

document.createElement('img').__proto__

可选链操作符

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

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

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

非空断言

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

  • 如果我们明确的知道对象的属性一定不会为空,那么可以使用非空断言 !
// 告诉 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 都提供了声明文件。
  • 比如,在使用数组时,数组所有方法都会有相应的代码提示以及类型信息。
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 会给出明确的提示。
    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

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

declare let count: numberdeclare let songName: stringinterface Position {
    x: number
    y: number
}
​
declare let position: Positiondeclare function add(x: number, y: number): numbertype Direction = 'left' | 'right' | 'top' | 'bottom'declare function changeDirection(direction: Direction): voidtype FomartPoint = (point: Position) => voiddeclare const fomartPoint: FomartPointexport { count, songName, position, add, changeDirection, FomartPoint, fomartPoint }

综合练习

Axios 与 TypeScript

<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>

静态结构

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

src/main.ts

import './styles/index.css'

styles/index.css

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

<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

<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

<script setup lang="ts">
    import Channel from './components/Channel.vue'
    import NewsList from './components/NewsList.vue'
</script><template>
    <Channel />
    <NewsList />
</template>

接口说明

获取频道列表:geek.itheima.net/v1_0/channe…

获取频道新闻:geek.itheima.net/v1_0/articl…频道id&timestamp=时间戳。

Pinia 环境搭建

  1. 配置 pinia,src/main.ts
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 模块,src/store/modules/channel.ts
import { defineStore } from 'pinia'const useChannelStore = defineStore('channel', {
    state() {
        return {}
    },
    getters: {},
    actions: {},
})
​
export default useChannelStore
  1. 创建根 store 并关联 channel 模块,src/store/index.ts
import useChannelStore from './modules/channel'export default function useStore() {
    return {
        channel: useChannelStore(),
    }
}

获取频道

  1. 创建频道列表类型和 ApiResponse,src/types/data.d.ts
export type ChannelList = {
    id: number
    name: string
}[]
​
export type ApiResponse<T> = {
    message: string
    data: T
}
  1. 定义请求数据的 action 并存储到 state,src/store/modules/channel.ts
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,src/components/Channel.vue
<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,src/store/modules/channel.ts
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 和点击高亮,src/components/Channel.vue
<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. 定义文章列表类型,src/types/data.d.ts
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 模块,src/store/modules/news.ts
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 模块到跟模块,src/store/index.ts
import useChannelStore from './modules/channel'
import useNewsStore from './modules/news'export default function useStore() {
    return {
        channel: useChannelStore(),
        news: useNewsStore(),
    }
}
  1. 监听 channel.active,发起请求并渲染,src/components/NewsList.vue
<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>