第9章 TypeScript集成
第27节 TypeScript进阶
9.27.1 泛型在Vue中的应用
泛型是TypeScript中的重要特性,它允许我们创建可重用的组件和函数,这些组件和函数可以处理多种类型的数据。在Vue中,泛型可以用于组件、组合式函数、Props等多个场景。
泛型组件
我们可以创建泛型组件,使其能够处理不同类型的数据:
<template>
<div class="list-container">
<h2>{{ title }}</h2>
<ul>
<li v-for="(item, index) in items" :key="index">
<slot :item="item" :index="index"></slot>
</li>
</ul>
</div>
</template>
<script setup lang="ts" generic="T">
interface Props<T> {
title: string
items: T[]
}
const props = defineProps<Props<T>>()
</script>在父组件中使用泛型组件:
<template>
<div>
<!-- 字符串列表 -->
<GenericList title="字符串列表" :items="stringItems">
<template #default="{ item }">
{{ item }}
</template>
</GenericList>
<!-- 数字列表 -->
<GenericList title="数字列表" :items="numberItems">
<template #default="{ item }">
{{ item }}
</template>
</GenericList>
<!-- 用户列表 -->
<GenericList title="用户列表" :items="userItems">
<template #default="{ item }">
{{ item.name }} - {{ item.email }}
</template>
</GenericList>
</div>
</template>
<script setup lang="ts">
import GenericList from './GenericList.vue'
interface User {
name: string
email: string
}
const stringItems = ['item1', 'item2', 'item3']
const numberItems = [1, 2, 3, 4, 5]
const userItems: User[] = [
{ name: '张三', email: 'zhangsan@example.com' },
{ name: '李四', email: 'lisi@example.com' }
]
</script>泛型Props
在定义Props时,我们可以使用泛型来增强类型安全性:
<template>
<div>
<h1>{{ title }}</h1>
<div v-if="value" class="value">
{{ value }}
</div>
<div v-else class="empty">
无数据
</div>
</div>
</template>
<script setup lang="ts" generic="T">
interface Props<T> {
title: string
value: T | null
}
const props = defineProps<Props<T>>()
</script>泛型组合式函数
泛型在组合式函数中也非常有用,可以创建更通用的逻辑:
// composables/usePagination.ts
import { ref, computed } from 'vue'
export interface PaginationOptions {
pageSize?: number
}
export function usePagination<T>(items: T[], options: PaginationOptions = {}) {
const { pageSize = 10 } = options
const currentPage = ref(1)
const totalPages = computed(() => {
return Math.ceil(items.length / pageSize)
})
const paginatedItems = computed(() => {
const start = (currentPage.value - 1) * pageSize
const end = start + pageSize
return items.slice(start, end)
})
const goToPage = (page: number) => {
currentPage.value = Math.max(1, Math.min(page, totalPages.value))
}
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++
}
}
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--
}
}
return {
currentPage,
totalPages,
paginatedItems,
goToPage,
nextPage,
prevPage
}
}在组件中使用:
<template>
<div>
<h1>分页示例</h1>
<ul>
<li v-for="(item, index) in paginatedItems" :key="index">
{{ item.name }} - {{ item.email }}
</li>
</ul>
<div class="pagination">
<button @click="prevPage" :disabled="currentPage === 1">上一页</button>
<span>{{ currentPage }} / {{ totalPages }}</span>
<button @click="nextPage" :disabled="currentPage === totalPages">下一页</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { usePagination } from '../composables/usePagination'
interface User {
name: string
email: string
}
const users = ref<User[]>([
{ name: '张三', email: 'zhangsan@example.com' },
{ name: '李四', email: 'lisi@example.com' },
{ name: '王五', email: 'wangwu@example.com' },
// 更多用户...
])
const { currentPage, totalPages, paginatedItems, nextPage, prevPage } = usePagination(users.value, { pageSize: 2 })
</script>9.27.2 类型工具使用
TypeScript提供了许多内置的类型工具,可以帮助我们创建和转换类型。以下是一些常用的类型工具:
Pick 和 Omit
Pick<T, K>:从类型T中选择指定的属性K,创建一个新类型Omit<T, K>:从类型T中排除指定的属性K,创建一个新类型
interface User {
id: number
name: string
email: string
password: string
createdAt: Date
}
// 只选择id、name和email属性
type UserPublic = Pick<User, 'id' | 'name' | 'email'>
// 等价于:
// interface UserPublic {
// id: number;
// name: string;
// email: string;
// }
// 排除password属性
type UserWithoutPassword = Omit<User, 'password'>
// 等价于:
// interface UserWithoutPassword {
// id: number;
// name: string;
// email: string;
// createdAt: Date;
// }在组件中使用:
<script setup lang="ts">
import type { User } from '../types'
type UserPublic = Pick<User, 'id' | 'name' | 'email'>
const fetchUser = async (id: number): Promise<UserPublic> => {
const response = await fetch(`/api/users/${id}`)
const user = await response.json()
// 只返回公开信息
return { id: user.id, name: user.name, email: user.email }
}
</script>Extract 和 Exclude
Extract<T, U>:从类型T中提取可以赋值给类型U的类型Exclude<T, U>:从类型T中排除可以赋值给类型U的类型
type EventType = 'click' | 'hover' | 'focus' | 'change' | 'submit'
type FormEventType = 'change' | 'submit' | 'reset'
// 提取同时属于EventType和FormEventType的类型
type CommonEvent = Extract<EventType, FormEventType> // 'change' | 'submit'
// 从EventType中排除FormEventType
type NonFormEvent = Exclude<EventType, FormEventType> // 'click' | 'hover' | 'focus'在组件中使用:
<template>
<div>
<button @click="handleEvent('click')">点击</button>
<input @change="handleEvent('change')" placeholder="输入内容" />
<form @submit.prevent="handleEvent('submit')">
<button type="submit">提交</button>
</form>
</div>
</template>
<script setup lang="ts">
type EventType = 'click' | 'hover' | 'focus' | 'change' | 'submit'
type FormEventType = 'change' | 'submit'
type NonFormEvent = Exclude<EventType, FormEventType> // 'click' | 'hover' | 'focus'
const handleEvent = (eventType: EventType) => {
if (isFormEvent(eventType)) {
console.log('表单事件:', eventType)
} else {
console.log('非表单事件:', eventType)
}
}
// 类型守卫
function isFormEvent(event: EventType): event is FormEventType {
return ['change', 'submit'].includes(event)
}
</script>Partial 和 Required
Partial<T>:将类型T的所有属性变为可选Required<T>:将类型T的所有属性变为必填
interface User {
id: number
name: string
email: string
age?: number
isActive?: boolean
}
// 所有属性变为可选
type PartialUser = Partial<User>
// 等价于:
// interface PartialUser {
// id?: number;
// name?: string;
// email?: string;
// age?: number;
// isActive?: boolean;
// }
// 所有属性变为必填
type RequiredUser = Required<User>
// 等价于:
// interface RequiredUser {
// id: number;
// name: string;
// email: string;
// age: number;
// isActive: boolean;
// }在组件中使用:
<script setup lang="ts">
interface User {
id: number
name: string
email: string
age?: number
isActive?: boolean
}
// 更新用户时,只需要提供部分属性
type UpdateUserDto = Partial<Omit<User, 'id'>>
const updateUser = async (id: number, data: UpdateUserDto) => {
const response = await fetch(`/api/users/${id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
return response.json()
}
// 使用示例
updateUser(1, { name: '新名称' }) // 只更新名称
updateUser(1, { email: 'new@email.com', isActive: true }) // 更新邮箱和激活状态
</script>Record
Record<K, T>:创建一个对象类型,其属性键为K,属性值为T
// 创建一个以字符串为键,数字为值的对象类型
type ScoreRecord = Record<string, number>
// 使用示例
const scores: ScoreRecord = {
'张三': 95,
'李四': 88,
'王五': 92
}
// 更复杂的Record类型
interface User {
name: string
email: string
}
type UserRecord = Record<number, User>
const users: UserRecord = {
1: { name: '张三', email: 'zhangsan@example.com' },
2: { name: '李四', email: 'lisi@example.com' }
}9.27.3 第三方库类型声明
当使用第三方库时,我们可能需要为其添加类型声明。TypeScript提供了几种方式来处理这种情况:
使用@types包
许多流行的第三方库都有对应的@types包,我们可以直接安装:
# 安装axios的类型声明
npm install -D @types/axios
# 安装lodash的类型声明
npm install -D @types/lodash自定义类型声明
对于没有@types包的库,我们可以创建自定义类型声明文件:
- 在项目根目录创建
types文件夹 - 在该文件夹中创建类型声明文件,如
my-library.d.ts
// types/my-library.d.ts
// 声明模块
declare module 'my-library' {
// 声明函数
export function myFunction(arg: string): string
// 声明类
export class MyClass {
constructor(options: MyOptions)
method(): void
}
// 声明接口
export interface MyOptions {
name: string
value?: number
}
}- 在
tsconfig.json中配置类型声明文件路径:
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./types"]
}
}模块扩展
我们可以扩展现有模块的类型:
// types/vue-router.d.ts
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
// 添加自定义元数据类型
title?: string
requiresAuth?: boolean
roles?: string[]
}
}这样我们就可以在路由配置中使用自定义的元数据:
const routes = [
{
path: '/admin',
component: () => import('../views/Admin.vue'),
meta: {
title: '后台管理',
requiresAuth: true,
roles: ['admin']
}
}
]9.27.4 类型安全的路由与状态管理
类型安全的Vue Router
Vue Router 4提供了良好的TypeScript支持,我们可以为路由添加类型安全:
- 定义路由类型:
// router/types.ts
export interface RouteParams {
// 定义所有路由参数类型
id: string
userId: string
productId: string
}- 配置路由:
// router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue')
},
{
path: '/users',
name: 'Users',
component: () => import('../views/Users.vue')
},
{
path: '/users/:id',
name: 'UserDetail',
component: () => import('../views/UserDetail.vue')
},
{
path: '/products/:productId',
name: 'ProductDetail',
component: () => import('../views/ProductDetail.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router- 在组件中使用类型安全的路由:
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
// 类型安全的路由参数
const userId = route.params.id // string | undefined
// 类型安全的路由跳转
const goToProduct = (productId: string) => {
router.push({
name: 'ProductDetail',
params: { productId } // 类型检查:必须提供productId
})
}
</script>类型安全的Pinia
Pinia天生支持TypeScript,我们可以为状态管理添加类型安全:
- 定义Store:
// stores/counter.ts
import { defineStore } from 'pinia'
interface CounterState {
count: number
name: string
isActive: boolean
}
export const useCounterStore = defineStore('counter', {
state: (): CounterState => ({
count: 0,
name: '计数器',
isActive: true
}),
getters: {
doubleCount: (state) => state.count * 2,
isPositive: (state) => state.count > 0
},
actions: {
increment(amount: number = 1) {
this.count += amount
},
reset() {
this.count = 0
this.isActive = true
}
}
})- 在组件中使用类型安全的Store:
<script setup lang="ts">
import { useCounterStore } from '../stores/counter'
const counterStore = useCounterStore()
// 类型安全的状态访问
const count = counterStore.count // number
const name = counterStore.name // string
// 类型安全的getters访问
const doubleCount = counterStore.doubleCount // number
const isPositive = counterStore.isPositive // boolean
// 类型安全的actions调用
const increment = () => {
counterStore.increment(5) // 类型检查:参数必须是number
}
const reset = () => {
counterStore.reset() // 类型检查:不需要参数
}
</script>最佳实践与注意事项
使用类型别名和接口:
- 对于复杂类型,使用类型别名或接口可以提高代码的可读性
- 接口可以被扩展,类型别名可以使用联合和交叉类型
合理使用泛型:
- 泛型可以提高代码的复用性,但不要过度使用
- 为泛型添加约束,避免过于宽泛的类型
使用类型守卫:
- 类型守卫可以帮助TypeScript进行更准确的类型推断
- 使用
is关键字创建自定义类型守卫
为第三方库添加类型声明:
- 优先使用@types包
- 对于没有@types包的库,创建自定义类型声明
利用TypeScript的自动类型推断:
- 对于简单类型,TypeScript可以自动推断,不需要显式注解
- 对于复杂类型,显式注解可以提高代码的可读性和安全性
定期更新TypeScript版本:
- 新版本的TypeScript提供了更多的类型特性和更好的性能
- 保持TypeScript版本与Vue和其他库兼容
小结
本节我们学习了TypeScript进阶内容,包括:
- 泛型在Vue组件、Props和组合式函数中的应用
- 常用类型工具:Pick、Omit、Extract、Exclude、Partial、Required、Record
- 第三方库类型声明的添加方式
- 类型安全的Vue Router和Pinia配置
通过掌握这些TypeScript进阶知识,我们可以创建更安全、更可维护的Vue应用。TypeScript的静态类型检查可以帮助我们在开发阶段发现潜在错误,提高代码质量和开发效率。
思考与练习
- 创建一个泛型组件,使其能够处理不同类型的数据列表。
- 使用类型工具(Pick、Omit、Partial等)创建不同的类型变体。
- 为一个第三方库添加自定义类型声明。
- 配置类型安全的Vue Router,包括路由参数和元数据。
- 创建一个类型安全的Pinia Store,并在组件中使用。
- 尝试使用泛型组合式函数处理不同类型的数据。