TS 知识点总结

356 阅读24分钟

第一板块 (知识点)

编译并运行 TS 代码

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

TS 特点

  1. TS 的提示功能很完善
  2. TS 对于有数据有明确的类型限制, 不必等到代码运行的时候才报错(会在之后写代码的过程中自动检测)

TS 中数据类型

  1. js 已有类型
  • 原始类型:number/string/boolean/null/undefined/symbol/bigint
  • 对象类型:object(包括,数组、对象、函数等对象)。
  1. TS 新增类型
  • 联合类型、自定义类型(类型别名)、接口、元组、字面量类型、枚举、void、any 等。
  • 注意:TS 中的原始类型和 JS 中写法一致;TS 中的对象类型在 JS 类型基础上更加细化,每个具体的对象(比如数组、对象、函数)都有自己的类型语法。

数组类型

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

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

联合类型

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

let arr: (number | string)[] = [1, 'abc', 2]
  • 解释:|(竖线)在 TS 中叫做联合类型,即由两个或多个其他类型组成的类型,表示可以是这些类型中的一种。
  • 注意:这是 TS 中联合类型的语法,只有一根竖线,不要与 JS 中的或(||)混淆了。
  • 场景:定时器的初始变量定义。
// 默认情况下不会对 nulL 和 undefined 进行类型检测
// 通过 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]
  1. 使用 type 关键字来创建自定义类型。

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

  3. 推荐使用大写字母开头。

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

函数类型

函数的类型实际上指的是:函数参数 和 返回值 的类型

  • 单独指定参数、返回值的类型。
// 函数声明
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) => number

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

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)
  • 可选参数语法:在可传可不传的参数名称后面添加 ?(问号)。
// start、end 可传可不传,传就传 number 类型
function mySlice(start?: number, end?: number): void {
    console.log('起始索引:', start, '结束索引:', end)
}
  • 注意:可选参数只能出现在参数列表的最后,也就是说可选参数后面不能再出现必选参数。

参数默认值

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

// 可选参数
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 属性就可以省略。
  • 可选属性的语法与函数可选参数的语法一致,都使用 ? 来表示。
    url: string
    method?: string
}

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

接口继承

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

  • 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 描述时不需要 =, 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 TSetPerson = {
    (name: string, age: number): void
}
const setPerson: TSetPerson = (name, age) => {}

setPerson('ifer', 18)
  • 都允许拓展,语法不一样(interface 是通过 extends 进行拓展, type 通过 & 符号进行拓展)
// 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 } & TName

const p: TPerson = {
    name: 'ifer',
    age: 18,
}
// type & interface
interface IName {
    name: string
}
type TPerson = { age: number } & IName

const 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]
  • 类型可以确切地标记出有多少个元素,以及每个元素的类型。

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

// 其实 useState 的返回值就是一个元组
function useState(num: number) {
    const setNum = (num: number) => {}
    return [num, setNum]
}
// 问题:这里 num 和 setNum 被推断成了 number | ((num: number) => void) 类型,一般期望 num 是 number 类型,setNum 是 ((num: number) => void) 类型
const [num, setNum] = useState(8)
// 解决:返回值明确指定为元组类型,或者通过后续学习的断言
function useState(num: number): [number, (number: number) => void] {
    const setNum = (num: number) => {}
    return [num, setNum]
}
const [num, setNum] = useState(8)
  • 语法补充:可以给元组中每一个元素起名字,见名知意。
// const arrTuple: [height: number, age: number, salary: number] = [170, 20, 17500]
function useState(num: number): [num: number, fn: (n: number) => void] {
    const setNum = (num: number): void => {}
    return [num, setNum]
}
const [num, setNum] = useState(10)
setNum(29)

类型推论

  • 在 TS 中,某些没有明确指出类型的地方,TS 的类型推论机制会帮助提供类型
  • 换句话说:由于类型推论的存在,这些地方,类型注解可以省略不写。
  • 常见的发生类型推论的 2 种场景:声明变量并初始化时;决定函数返回值时。
// 变量 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')

字面量类型

  • 变量 str1 的类型为:string,变量 str2 的类型为:’Hello TS’。
let str1 = 'Hello TS'
const str2 = 'Hello TS'
  • str1 是一个变量,它的值可以是任意字符串,所以类型为:string。
  • str2 是一个常量,它的值不能变化只能是 ‘Hello TS’,所以,它的类型为:’Hello TS’(字符串字面量类型)。
  • 注意:此处的 ‘Hello TS’,就是一个字符串字面量类型,也就是说某个特定的字符串也可以作为 TS 中的类型。
  • 任意的 JS 字面量都可以作为类型使用,例如 { name: 'jack' }[]18'abc'falsefunction() {} 等。

使用方式和场景

  • 使用方式:字面量类型常配合联合类型一起使用
  • 场景1:用来表示一组明确的可选值列表,比如在贪吃蛇游戏中,游戏方向的值只能是上、下、左、右中的一个。
type Direction = 'up' | 'down' | 'left' | 'right'
function changeDirection(direction: Direction) {
    console.log(direction)
}
changeDirection('up') // 调用函数时,会有类型提示
  • 解释:参数 direction 的值只能是 up/down/left/right 中的任意一个。

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

  • 场景2: 性别和 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': // 这里会自动具有提示
    }
}

枚举类型

  • 注意:枚举成员是有值的,默认为:从 0 开始自增的数值。
// 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 代码
  • 说明:枚举与前面讲到的字面量类型 + 联合类型组合的功能类似,都用来表示一组明确的可选值列表。
  • 推荐:字面量类型 + 联合类型组合的方式,因为相比枚举,这种方式更加直观、简洁、高效。
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.男,
}

类型断言

当有时候获取的元素不确定是什么数据类型时, 就需要手动指定数据类型, 后面的提示才会出来

  • 这种写法可以有 HTML 元素或者为 null
// 注意 document.querySelector('a') 这种写法会自动推断出是 HTMLLinkElement 类型
const oLink = document.getElementById('link')
  • 断言写法
const oLink = document.getElementById('link') as HTMLElement
  • 另一种语法,使用 <> 语法,这种语法形式不常用知道即可。
const oLink = <HTMLAnchorElement>document.getElementById('link')
  • 技巧:打开浏览器控制台,选中标签,通过 $0.__proto__ 可以获取 DOM 元素的类型。

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,所以下面两种情况下都应该提供类型)。

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

    • 函数参数不加类型。

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 和 unknown 比较 (面试题)

  • 任何类型可以给 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

函数重载

  • 方法 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)
  • 方法 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']))

关于泛型

  • 泛型:定义时宽泛、不确定的类型,需要使用者去主动传入。
  • 为了实现传入什么数据类型就返回该数据类型本身(也就是说,参数和返回值类型相同)。

泛型函数

  • 定义 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 中的一些常见操作。

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

    • Partial<Type>

    • Readonly<Type>

    • Pick<Type, Keys>

Partial

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

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

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

可能出面试题的点

  1. typeof 和 keyof
  2. any 和 unknown

注意

  1. TS 中的 | 表示或

第二板块 (结合项目的应用)

useState

const [name, setName] = useState<string>('张三')
const [age, setAge] = useState<number>(28)
const [isProgrammer, setIsProgrammer] = useState<boolean>(true)

// 如果你在 setName 函数中的参数不符合声明的变量类型,程序会报错
<button onClick={() => setName(100)}>按钮</button>

useEffect

  • useEffect 是用于我们管理副作用(例如 API 调用)并在组件中使用 React 生命周期的。
  • useEffect 函数不涉及到任何泛型参数,在 TS 中的使用和 JS 中完全一致。

📝 请求数据

  • 接口:http://geek.itheima.net/v1_0/channels
  • 需求:发送请求获取频道列表数据,并且渲染。
  • 注意:如果 useState 没有提供具体类型的初始值,是需要使用泛型参数指定类型的。
import { useEffect, useState } from 'react'
import axios from 'axios'
// 定义类型别名 Res
type Res = { id: number; name: string }[]
export default function App() {
    // 解决1:给个初始值,不推荐
    // const [list, setList] = useState([{ name: 'ifer', id: 0 }])
    // 解决2:泛型参数
    // 一般复杂的类型,需要手动进行指定初始值类型,TS 没法进行推断
    const [list, setList] = useState<Res>([])
    useEffect(() => {
        const fetchData = async () => {
            const res = await axios.get('http://geek.itheima.net/v1_0/channels')
            setList(res.data.data.channels)
        }
        fetchData()
    }, [])
    return (
        <ul>
            {list.map((item) => {
                return <li key={item.id}>{item.name}</li>
            })}
        </ul>
    )
}

useRef

  • useRef 接收一个泛型参数,泛型参数用于指定 current 属性的值的类型
  • 如果使用 useRef 操作 DOM,需要明确指定所操作的 DOM 的具体的类型,否则 current 属性会是 null。
  • 📝 需求:获取 input 的 value 和获取 a 标签的 href。
import { useRef } from 'react'
export default function App() {
    // 不推荐 any
    // const inputRef = useRef<any>(null)
    // 指定了 current 的类型,目的是为了让 current 有属性提示
    const inputRef = useRef<HTMLInputElement>(null)
    const aRef = useRef<HTMLAnchorElement>(null)
    const get = () => {
        // inputRef.current 可能是 null,所以用了 ?.
        console.log(inputRef.current?.value)
        console.log(aRef.current?.href)
    }
    return (
        <div>
            <input type='text' ref={inputRef} />
            <a href='https://www.baidu.com' ref={aRef}>
                百度
            </a>
            <button onClick={get}>获取</button>
        </div>
    )
}

非空断言

  • 如果我们明确的知道对象的属性一定不会为空,那么可以使用非空断言 !
  • 注意:非空断言一定要确保有该属性才能使用,不然使用非空断言会导致 Bug。
function show(name: string | undefined) {
    // name! 意思是从 name 可能的值中断言(假定)没有 null 和 undefined
    let sName: string = name!
}
  • 应用场景
import { useRef } from 'react'
export default function App() {
    const inputRef = useRef<HTMLInputElement>(null)
    const get = () => {
        // 断言 inputRef.current 不可能为空
        /* const current = inputRef.current!
        console.log(current.value) */
        console.log(inputRef.current!.value)
    }
    return (
        <div>
            <input type='text' ref={inputRef} />
            <button onClick={get}>获取</button>
        </div>
    )
}

React 路由

  • 安装:yarn add react-router-dom@5.3.0 @types/react-router-dom

useHistory

通过点击 useHistory 查看内部源代码, 发现 state 中数据是通过泛型参数约束

  • useHistory 在跳转时可以通过 state 进行传参,并通过泛型参数来指定 state 的类型。
const history = useHistory<{ from: string }>()
const login = () => {
    history.push({
        pathname: '/login',
        state: {
            from: 'ifer',
        },
    })
}
  • 🧐 分析如下
// 点击 useHistory 可以发现类型定义的源码
// 参数:HistoryLocationState
// 返回:H.History<HistoryLocationState>

// 点击 H:import * as H from 'history';

// 点进去 history

// 看到 HistoryLocationState 给了 LocationDescriptor<HistoryLocationState>,通过观察可以发现,也可以通过第二个参数进行 state 的参数传递,如下:
// push(location: Path | LocationDescriptor<HistoryLocationState>, state?: HistoryLocationState): void;

// 点击 LocationDescriptor,如下:
// export type LocationDescriptor<S = LocationState> = History.LocationDescriptor<S>;

// 点击 History.LocationDescriptor,如下:
// export type LocationDescriptor<S = LocationState> = Path | LocationDescriptorObject<S>;

// 点击 LocationDescriptorObject,如下:
/* export interface LocationDescriptorObject<S = LocationState> {
    pathname?: Pathname | undefined;
    search?: Search | undefined;
    state?: S | undefined;
    hash?: Hash | undefined;
    key?: LocationKey | undefined;
} */

// 发现泛型 S 确定对应了 state
export function useHistory<HistoryLocationState = H.LocationState>(): H.History<HistoryLocationState>

useLocation

  • useLocation 接收一个泛型参数,用于指定接收 state 的类型,与 useHistory 的泛型参数对应。
import { useLocation } from 'react-router-dom'

export default function Login() {
    const location = useLocation<{ from: string }>()
    // 直接点击登录页,没有传参会报错,所以这里用了可选链操作符 ?.
    return <div>Login: {location.state?.from}</div>
}
// Tip: 这里明确或了一个 null,当后面再书写 location.state.from 的时候,.from 的前面会自动加上 ? 号
export type LocationState = {
    from: string
} | null
  • 源码分析
// 点击 useLocation,把传递过来的泛型参数 S 给了返回值 H.Location<S>
// export function useLocation<S = H.LocationState>(): H.Location<S>;

// 点击 Location,发现 S 给了 state,由此推断,泛型参数是用来约束 state 参数的,如下:
/* export interface Location<S = LocationState> {
    pathname: Pathname;
    search: Search;
    state: S;
    hash: Hash;
    key?: LocationKey | undefined;
} */

useParams


import { useParams } from 'react-router'
export default function Article() {
    const params = useParams<{ id: string }>()
    return <div>Article: {params.id}</div>
}

useSelector

  • useSelector 的基本使用,接收两个泛型参数。
// 泛型参数1: 指定 state 的类型,默认是 {}
// 泛型参数2: 指定函数返回值的类型
const name = useSelector<{ name: string }, string>((state) => state.name)
  • 也可以不使用泛型,通过指定 state 函数参数的类型(推荐,类型推论友好),参考文档
const name = useSelector((state: { name: string }) => state.name)

RootState

  • 需求:如何准确的获取到 store 中 todo 的类型呢?参考文档
  • typeof 配合 ReturnType
  • typeof 可以获取某个数据的类型,ReturnType 是一个泛型工具类型,可以获取一个函数类型的返回值类型。
function fn(n1: number, n2: number): number {
    return n1 + n2
}
// 获取 fn 函数的类型
type Fn = typeof fn
// 获取 Fn 函数的返回值类型
type Res = ReturnType<Fn>

结合 TS 的 redux "常爱瑞组"

  • 常 (types/store.d.ts)
import store from '../store'

export type TodoAction =
    | {
          type: 'TODO_ADD'
          name: string
          id: number
          done: boolean
      }
    | {
          type: 'TODO_DEL'
          id: number
      }
    | {
          type: 'TODO_CHANGE_DOEN'
          id: number
      }

export type RootState = ReturnType<typeof store.getState>
  • 爱 (store/actions/todo.ts)
import { TodoAction } from '../../types/store'

export const todoAdd = (name: string): TodoAction => ({
    type: 'TODO_ADD',
    name,
    id: Date.now(),
    done: false,
})
export const todoDel = (id: number): TodoAction => ({
    type: 'TODO_DEL',
    id,
})

export const todoChangeDone = (id: number): TodoAction => ({
    type: 'TODO_CHANGE_DOEN',
    id,
})
  • 瑞 (store/reducers/todo.ts)
import { TodoItem } from '../../types/data'
import { TodoAction } from '../../types/store'

// 明确指定类型,防止当数组中为空的时候报错
const initState: TodoItem[] = []
export default function todo(state = initState, action: TodoAction) {
    switch (action.type) {
        case 'TODO_ADD':
            const { type, ...rest } = action
            return [rest, ...state]
        case 'TODO_DEL':
            return state.filter((item) => item.id !== action.id)
        case 'TODO_CHANGE_DOEN':
            return state.map((item) => (item.id === action.id ? { ...item, done: !item.done } : item))
        default:
            return state
    }
}
  • 组(src/App.tsx)
import { useSelector, useDispatch } from 'react-redux'
import { RootState } from './types/store'
import './App.css'
import { useState } from 'react'
import { todoAdd, todoChangeDone, todoDel } from './store/actions/todo'

export default function App() {
    const dispatch = useDispatch()
    const [name, setName] = useState('')
    const todos = useSelector((state: RootState) => state.todo)
    const handleAdd = (e: React.KeyboardEvent<HTMLInputElement>) => {
        if (e.code === 'Enter') {
            dispatch(todoAdd(name))
            setName('')
        }
    }
    const handleDel = (id: number) => dispatch(todoDel(id))
    // #2
    const handleChangeDone = (id: number) => dispatch(todoChangeDone(id))
    return (
        <>
            <input type='text' value={name} onChange={(e) => setName(e.target.value)} onKeyUp={handleAdd} />
            <ul>
                {todos.map((item) => (
                    <li className={item.done ? 'completed' : ''} key={item.id}>
                        {/* #1 */}
                        <span style={{ userSelect: 'none' }} onDoubleClick={() => handleChangeDone(item.id)}>
                            {item.name}
                        </span>
                        <button onClick={() => handleDel(item.id)}>x</button>
                    </li>
                ))}
            </ul>
        </>
    )
}

redux-thunk

ThunkAction

// 泛型参数
// 1: 指定内部函数的返回值类型,一般是 void
// 2: 指定 RootState 的类型
// 3: 指定额外的参数类型,这里用不到,一般为 unknown 或 any,可以在配置 redux-thunk 的时候,通过 thunk.withExtraArgument('ifer') 指定
// 4: 指定 dispatch 的 action 的类型
import { ThunkAction } from 'redux-thunk'
export const todoDelAsync = (id: number): ThunkAction<void, RootState, unknown, TodoAction> => {
    return (dispatch, getState, extraData) => {
        // getState().todo // 因为,指定了 RootState 类型,这儿自动具有提示
        setTimeout(() => {
            dispatch(todoDel(id))
        }, 2000)
    }
}
  • 可以把 RootThunkAction 抽取到 types/store.d.ts 文件中
import { ThunkAction } from 'redux-thunk'
import store from '../store'

export type TodoAction =
    | {
          type: 'TODO_ADD'
          name: string
          id: number
          done: boolean
      }
    | {
          type: 'TODO_DEL'
          id: number
      }
    | {
          type: 'TODO_CHANGE_DOEN'
          id: number
      }

export type RootState = ReturnType<typeof store.getState>
export type RootThunkAction = ThunkAction<void, RootState, unknown, TodoAction>
  • 问题说明:在 redux-thunk@2.4.0 新版中,使用 dispatch 的时候,会丢失提示,需要降级到 2.3.0 版本,issues
  • 解决办法:yarn add redux-thunk@2.3.0

types 文件夹中

  • data.d.ts 文件, 存放公共 type 和 interface
export interface IResponse<T> {
  data: T
  message: string
}

export interface ChannelItem {
  id: number
  name: string
}

export interface Cover {
  type: number
  images: string[]
}

// 每一个文章
export interface ArticleItem {
  art_id: string
  title: string
  aut_id: string
  comm_count: number
  pubdate: string
  aut_name: string
  is_top: number
  cover: Cover
}
  • store.d.ts 用来存放 每个模块的 actions 类型 | RootAction | RootState | RootThunkAction
import { ThunkAction } from 'redux-thunk'
import { ArticleItem, ChannelItem } from './data'
import store from '../store'

// ChannelAction
export type ChannelAction =
  | {
      type: 'CHANNEL_SAVE'
      payload: ChannelItem[]
    }
  | {
      type: 'CHANNEL_ACTIVE'
      id: number
    }

export type ArticleAction = {
  type: 'ARTICLE_SAVE'
  payload: ArticleItem[]
}

// RootAction
export type RootAction = ChannelAction | ArticleAction

// RootState
export type RootState = ReturnType<typeof store.getState>

// RootThunkAction
export type RootThunkAction = ThunkAction<void, RootState, unknown, RootAction>

补充

  1. 可选链操作符
// 能在保证有 obj.first 的情况下采取获取 second 属性,没有的话也不至于报错(会返回 undefined)
let nestedProp = obj.first?.second
// 等价于
let nestedProp = obj.first === null || obj.first === undefined ? undefined : obj.first.second

注意

  1. 如果要对第三方包的返回数据进行 ts 管理, 就需要一层层找到设置变量的地方

比如说这里要利用 ts 特性, 在书写代码时能直接出现 from 的他提示, 就需要手动设置 TState= { from : string }, 通过点击进入 useLocation ,一层层找到 state 设置的地方, 发现 state 的参数受泛型参数的约束, 所以通过设定泛型参数就能实现参数代码提示, 这里的 TState 是从 .d.ts文件中设置并导出的类型变量 image.png

  1. 这种方式拿到 state 数据的类型并导出 export type RootState = ReturnType<typeof store.getState>\