都2022年了,你还不会TS吗?靓仔!快来看看我的笔记吧!! TypeScript (整理学习NO.02)

293 阅读12分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第13天,点击查看活动详情

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

image.png

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

image.png