56. 组合式函数的类型化
📖 概述
组合式函数是Vue 3组合式API的核心特性之一,它们允许我们封装和复用逻辑。正确的类型化对于确保组合式函数的类型安全至关重要。本集将深入讲解组合式函数的类型化方法,包括基本类型化、泛型组合式函数、返回值类型化、参数类型化等,帮助你编写更加安全、可靠的Vue 3 + TypeScript组合式函数。
✨ 核心知识点
1. 基本组合式函数的类型化
简单组合式函数
<script setup lang="ts">
import { ref, computed } from 'vue'
// 简单的组合式函数
function useCounter(initialValue: number = 0) {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
return {
count,
increment,
decrement,
reset
}
}
// 使用组合式函数
const { count, increment, decrement, reset } = useCounter(5)
// 类型安全的使用
console.log(count.value.toFixed(0)) // 正确,count.value被推断为number类型
increment() // 正确,increment是无参数函数带返回值类型标注的组合式函数
<script setup lang="ts">
import { ref, computed } from 'vue'
// 定义返回值类型
interface CounterReturn {
count: Ref<number>
increment: () => void
decrement: () => void
reset: () => void
doubleCount: ComputedRef<number>
}
// 带返回值类型标注的组合式函数
function useCounter(initialValue: number = 0): CounterReturn {
const count = ref(initialValue)
const doubleCount = computed(() => count.value * 2)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
return {
count,
increment,
decrement,
reset,
doubleCount
}
}
// 使用组合式函数
const { count, doubleCount, increment } = useCounter(5)
console.log(doubleCount.value) // 正确,doubleCount.value被推断为number类型2. 泛型组合式函数
基本泛型组合式函数
<script setup lang="ts">
import { ref, Ref } from 'vue'
// 泛型组合式函数
function useArray<T>(initialValue: T[] = []) {
const array = ref<T[]>(initialValue)
const push = (item: T) => array.value.push(item)
const pop = () => array.value.pop()
const remove = (index: number) => array.value.splice(index, 1)
const clear = () => array.value = []
return {
array,
push,
pop,
remove,
clear
}
}
// 使用泛型组合式函数
const { array: numbers, push: pushNumber } = useArray<number>([1, 2, 3])
pushNumber(4) // 正确,只能推送number类型
console.log(numbers.value) // 输出:[1, 2, 3, 4]
const { array: strings, push: pushString } = useArray<string>(['a', 'b', 'c'])
pushString('d') // 正确,只能推送string类型
console.log(strings.value) // 输出:['a', 'b', 'c', 'd']泛型约束组合式函数
<script setup lang="ts">
import { ref, Ref } from 'vue'
// 定义泛型约束
interface WithId {
id: number
}
// 带泛型约束的组合式函数
function useCrud<T extends WithId>(initialItems: T[] = []) {
const items = ref<T[]>(initialItems)
const add = (item: Omit<T, 'id'>) => {
const newItem = {
...item,
id: Math.max(...items.value.map(i => i.id), 0) + 1
} as T
items.value.push(newItem)
return newItem
}
const update = (id: number, updates: Partial<T>) => {
const index = items.value.findIndex(item => item.id === id)
if (index !== -1) {
items.value[index] = { ...items.value[index], ...updates }
return items.value[index]
}
return null
}
const remove = (id: number) => {
const index = items.value.findIndex(item => item.id === id)
if (index !== -1) {
const removed = items.value.splice(index, 1)
return removed[0]
}
return null
}
const find = (id: number) => {
return items.value.find(item => item.id === id) || null
}
return {
items,
add,
update,
remove,
find
}
}
// 使用带泛型约束的组合式函数
interface User {
id: number
name: string
email: string
}
const { items: users, add: addUser, update: updateUser } = useCrud<User>([
{ id: 1, name: '张三', email: 'zhangsan@example.com' }
])
// 正确使用
const newUser = addUser({ name: '李四', email: 'lisi@example.com' })
console.log(newUser.id) // 输出:2
const updatedUser = updateUser(1, { name: '张三更新' })
console.log(updatedUser?.name) // 输出:张三更新3. 组合式函数的参数类型化
基本参数类型化
<script setup lang="ts">
import { ref, watch } from 'vue'
// 基本参数类型化
function useDebounce<T>(
value: Ref<T>,
delay: number = 300
): Ref<T> {
const debouncedValue = ref<T>(value.value)
let timeoutId: number | null = null
watch(value, (newValue) => {
if (timeoutId) {
clearTimeout(timeoutId)
}
timeoutId = window.setTimeout(() => {
debouncedValue.value = newValue
}, delay)
})
return debouncedValue
}
// 使用带参数类型化的组合式函数
const searchQuery = ref('')
const debouncedSearchQuery = useDebounce(searchQuery, 500)
// 正确使用
searchQuery.value = 'vue'
console.log(debouncedSearchQuery.value) // 输出:'' (尚未防抖)可选参数与默认值
<script setup lang="ts">
import { ref, computed } from 'vue'
// 可选参数与默认值
interface UsePaginationOptions {
pageSize?: number
initialPage?: number
}
function usePagination(
totalItems: number,
options: UsePaginationOptions = {}
) {
const { pageSize = 10, initialPage = 1 } = options
const currentPage = ref(initialPage)
const pageSizeRef = ref(pageSize)
const totalPages = computed(() => {
return Math.ceil(totalItems / pageSizeRef.value)
})
const next = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++
}
}
const prev = () => {
if (currentPage.value > 1) {
currentPage.value--
}
}
const goToPage = (page: number) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
return {
currentPage,
pageSize: pageSizeRef,
totalPages,
next,
prev,
goToPage
}
}
// 使用带可选参数的组合式函数
const { currentPage, next, prev } = usePagination(100, { pageSize: 20 })
console.log(currentPage.value) // 输出:1
next()
console.log(currentPage.value) // 输出:24. 组合式函数的返回值类型推断
自动返回值类型推断
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
// 自动返回值类型推断
function useMousePosition() {
const x = ref(0)
const y = ref(0)
const updatePosition = (event: MouseEvent) => {
x.value = event.clientX
y.value = event.clientY
}
onMounted(() => {
window.addEventListener('mousemove', updatePosition)
})
onUnmounted(() => {
window.removeEventListener('mousemove', updatePosition)
})
return {
x,
y
}
}
// 使用组合式函数,返回值类型自动推断
const { x, y } = useMousePosition()
console.log(x.value, y.value) // 正确,x.value和y.value被推断为number类型显式返回值类型
<script setup lang="ts">
import { ref, Ref } from 'vue'
// 显式返回值类型
interface UseLocalStorageReturn<T> {
value: Ref<T>
set: (newValue: T) => void
remove: () => void
refresh: () => void
}
// 带显式返回值类型的组合式函数
function useLocalStorage<T>(
key: string,
initialValue: T
): UseLocalStorageReturn<T> {
const value = ref<T>(() => {
const stored = localStorage.getItem(key)
return stored ? JSON.parse(stored) : initialValue
})
const set = (newValue: T) => {
value.value = newValue
localStorage.setItem(key, JSON.stringify(newValue))
}
const remove = () => {
localStorage.removeItem(key)
value.value = initialValue
}
const refresh = () => {
const stored = localStorage.getItem(key)
if (stored) {
value.value = JSON.parse(stored)
}
}
return {
value,
set,
remove,
refresh
}
}
// 使用带显式返回值类型的组合式函数
const { value: userName, set: setUserName } = useLocalStorage<string>('userName', 'Guest')
setUserName('张三') // 正确,只能设置string类型
console.log(userName.value) // 输出:张三5. 组合式函数的依赖注入类型化
带依赖注入的组合式函数
<script setup lang="ts">
import { ref, inject, provide, type InjectionKey } from 'vue'
// 定义注入键类型
interface User {
id: number
name: string
email: string
}
// 创建注入键
const UserKey: InjectionKey<User> = Symbol('User')
// 提供依赖
provide(UserKey, {
id: 1,
name: '张三',
email: 'zhangsan@example.com'
})
// 带依赖注入的组合式函数
function useCurrentUser() {
const user = inject(UserKey)
if (!user) {
throw new Error('User not provided')
}
const updateName = (name: string) => {
user.name = name
}
return {
user,
updateName
}
}
// 使用带依赖注入的组合式函数
const { user, updateName } = useCurrentUser()
console.log(user.name) // 输出:张三
updateName('李四')
console.log(user.name) // 输出:李四带默认值的依赖注入
<script setup lang="ts">
import { inject, type InjectionKey } from 'vue'
// 定义注入键类型
interface Config {
apiUrl: string
timeout: number
}
// 创建注入键
const ConfigKey: InjectionKey<Config> = Symbol('Config')
// 带默认值的依赖注入
function useConfig() {
const config = inject(ConfigKey, {
apiUrl: 'https://api.example.com',
timeout: 5000
})
return {
config
}
}
// 使用带默认值的依赖注入
const { config } = useConfig()
console.log(config.apiUrl) // 输出:https://api.example.com🚀 实战案例
1. 复杂组合式函数实战
useFetch组合式函数
<script setup lang="ts">
import { ref, Ref, watch } from 'vue'
// 定义返回值类型
interface UseFetchReturn<T> {
data: Ref<T | null>
loading: Ref<boolean>
error: Ref<string | null>
refetch: () => Promise<void>
}
// 复杂组合式函数:useFetch
function useFetch<T>(url: Ref<string> | string): UseFetchReturn<T> {
const data = ref<T | null>(null)
const loading = ref(false)
const error = ref<string | null>(null)
const fetchData = async () => {
loading.value = true
error.value = null
try {
const response = await fetch(typeof url === 'string' ? url : url.value)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const result = await response.json()
data.value = result
} catch (err) {
error.value = (err as Error).message
data.value = null
} finally {
loading.value = false
}
}
// 监听url变化,重新请求
if (typeof url !== 'string') {
watch(url, fetchData)
}
// 初始请求
fetchData()
return {
data,
loading,
error,
refetch: fetchData
}
}
// 使用useFetch组合式函数
interface Post {
id: number
title: string
body: string
userId: number
}
const postId = ref(1)
const postUrl = computed(() => `https://jsonplaceholder.typicode.com/posts/${postId.value}`)
const { data: post, loading, error, refetch } = useFetch<Post>(postUrl)
// 正确使用
if (loading.value) {
console.log('Loading...')
}
if (error.value) {
console.error('Error:', error.value)
}
if (post.value) {
console.log(post.value.title) // 正确,post.value.title被推断为string类型
}
// 重新请求
postId.value = 2 // 自动重新请求
// 或手动重新请求
refetch()📝 最佳实践
优先使用自动类型推断
- TypeScript可以自动推断组合式函数的返回值类型
- 减少显式类型标注,提高开发效率
- 只在必要时使用显式类型标注
使用接口或类型别名定义复杂类型
- 提高代码的可读性和可维护性
- 便于复用和扩展
- 清晰的类型定义
合理使用泛型
- 提高组合式函数的复用性
- 保持类型安全
- 支持多种数据类型
为参数添加类型标注
- 提高函数的可读性和可维护性
- 帮助TypeScript进行更精确的类型检查
- 便于IDE提供更好的自动补全
处理可选参数和默认值
- 使用接口定义可选参数
- 为可选参数提供合理的默认值
- 提高函数的易用性
考虑错误处理
- 在组合式函数中处理可能的错误
- 返回错误信息,便于调用者处理
- 提高函数的健壮性
遵循单一职责原则
- 每个组合式函数只负责一个功能
- 提高函数的可维护性和可测试性
- 便于组合使用
💡 常见问题与解决方案
组合式函数返回值类型推断不准确
- 检查函数的返回值是否与预期一致
- 尝试添加显式返回值类型标注
- 检查TypeScript版本是否支持最新的类型推断特性
泛型组合式函数类型不生效
- 确保使用了正确的泛型语法
- 检查泛型约束是否正确
- 确保TypeScript版本支持泛型特性
依赖注入类型错误
- 确保使用了正确的注入键类型
- 检查注入键是否已提供
- 考虑为依赖注入添加默认值
组合式函数参数类型不匹配
- 检查函数调用时提供的参数类型是否与定义一致
- 尝试添加显式参数类型标注
- 考虑使用可选参数或默认值
组合式函数内部类型错误
- 检查函数内部的变量和函数调用是否符合类型定义
- 尝试添加显式类型标注
- 检查TypeScript配置是否正确
组合式函数之间的类型冲突
- 确保组合式函数的返回值类型不冲突
- 考虑使用命名空间或前缀避免命名冲突
- 检查组合式函数的依赖关系
📚 进一步学习资源
🎯 课后练习
基础练习
- 创建不同类型的组合式函数,添加正确的类型标注
- 练习使用泛型组合式函数
- 尝试创建带依赖注入的组合式函数
进阶练习
- 实现一个完整的useFetch组合式函数,支持GET、POST等HTTP方法
- 创建一个useForm组合式函数,支持表单验证和提交
- 实现一个usePagination组合式函数,支持分页逻辑
实战练习
- 重构一个现有的Vue 3项目,将逻辑封装为类型化的组合式函数
- 优化组合式函数的类型定义,提高类型安全性
- 解决项目中存在的类型错误
类型系统练习
- 实现复杂的组合式函数类型定义,包括泛型、联合类型、交叉类型等
- 测试组合式函数类型的边界情况
- 优化类型定义,提高类型推断的效果
通过本集的学习,你已经掌握了Vue 3中组合式函数的类型化方法和最佳实践。在实际开发中,正确的类型化可以提高组合式函数的类型安全性、可读性和可维护性,是Vue 3 + TypeScript开发的重要技能。下一集我们将深入学习泛型在Vue组件中的应用,进一步提升Vue 3 + TypeScript的开发能力。