持续创作,加速成长!这是我参与「掘金日新计划 · 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')
- 添加了第二个类型变量 Key,两个类型变量之间使用
,逗号分隔。 - keyof 关键字接收一个对象类型,生成其键名称的联合类型,例如这里也就是:
'name' | 'age'。 - 类型变量 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]
},
}
- 在接口名称的后面添加
<类型变量>,那么,这个接口就变成了泛型接口。 - 接口的类型变量,对接口中所有其他成员可见,也就是接口中所有成员都可以使用类型变量。
- 使用泛型接口时,需要显式指定具体的类型(比如,此处的
IdFunc<number>)。 - 此时,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 使用。
- 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>
- 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>
- 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 使用。
- 自定义事件,
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>
- 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 如何使用。
- 通过泛型指定 value 的值类型,如果是简单值,该类型可以省略。
const money = ref<number>(10)
const money = ref(10)
- 如果是复杂类型,可以通过泛型来指定初始值的类型。
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 就会自动加载库自己的类型声明文件,以提供该库的类型声明。
- 比如 axios,通过查看
-
由 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 文件提供类型声明。
- 创建
index.d.ts类型声明文件。 - 创建需要共享的类型,并使用 export 导出(TS 中的类型也可以使用 import/export 实现模块化功能)。
- 在需要使用共享类型的 .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 }