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) // 输出:2

4. 组合式函数的返回值类型推断

自动返回值类型推断

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

📝 最佳实践

  1. 优先使用自动类型推断

    • TypeScript可以自动推断组合式函数的返回值类型
    • 减少显式类型标注,提高开发效率
    • 只在必要时使用显式类型标注
  2. 使用接口或类型别名定义复杂类型

    • 提高代码的可读性和可维护性
    • 便于复用和扩展
    • 清晰的类型定义
  3. 合理使用泛型

    • 提高组合式函数的复用性
    • 保持类型安全
    • 支持多种数据类型
  4. 为参数添加类型标注

    • 提高函数的可读性和可维护性
    • 帮助TypeScript进行更精确的类型检查
    • 便于IDE提供更好的自动补全
  5. 处理可选参数和默认值

    • 使用接口定义可选参数
    • 为可选参数提供合理的默认值
    • 提高函数的易用性
  6. 考虑错误处理

    • 在组合式函数中处理可能的错误
    • 返回错误信息,便于调用者处理
    • 提高函数的健壮性
  7. 遵循单一职责原则

    • 每个组合式函数只负责一个功能
    • 提高函数的可维护性和可测试性
    • 便于组合使用

💡 常见问题与解决方案

  1. 组合式函数返回值类型推断不准确

    • 检查函数的返回值是否与预期一致
    • 尝试添加显式返回值类型标注
    • 检查TypeScript版本是否支持最新的类型推断特性
  2. 泛型组合式函数类型不生效

    • 确保使用了正确的泛型语法
    • 检查泛型约束是否正确
    • 确保TypeScript版本支持泛型特性
  3. 依赖注入类型错误

    • 确保使用了正确的注入键类型
    • 检查注入键是否已提供
    • 考虑为依赖注入添加默认值
  4. 组合式函数参数类型不匹配

    • 检查函数调用时提供的参数类型是否与定义一致
    • 尝试添加显式参数类型标注
    • 考虑使用可选参数或默认值
  5. 组合式函数内部类型错误

    • 检查函数内部的变量和函数调用是否符合类型定义
    • 尝试添加显式类型标注
    • 检查TypeScript配置是否正确
  6. 组合式函数之间的类型冲突

    • 确保组合式函数的返回值类型不冲突
    • 考虑使用命名空间或前缀避免命名冲突
    • 检查组合式函数的依赖关系

📚 进一步学习资源

🎯 课后练习

  1. 基础练习

    • 创建不同类型的组合式函数,添加正确的类型标注
    • 练习使用泛型组合式函数
    • 尝试创建带依赖注入的组合式函数
  2. 进阶练习

    • 实现一个完整的useFetch组合式函数,支持GET、POST等HTTP方法
    • 创建一个useForm组合式函数,支持表单验证和提交
    • 实现一个usePagination组合式函数,支持分页逻辑
  3. 实战练习

    • 重构一个现有的Vue 3项目,将逻辑封装为类型化的组合式函数
    • 优化组合式函数的类型定义,提高类型安全性
    • 解决项目中存在的类型错误
  4. 类型系统练习

    • 实现复杂的组合式函数类型定义,包括泛型、联合类型、交叉类型等
    • 测试组合式函数类型的边界情况
    • 优化类型定义,提高类型推断的效果

通过本集的学习,你已经掌握了Vue 3中组合式函数的类型化方法和最佳实践。在实际开发中,正确的类型化可以提高组合式函数的类型安全性、可读性和可维护性,是Vue 3 + TypeScript开发的重要技能。下一集我们将深入学习泛型在Vue组件中的应用,进一步提升Vue 3 + TypeScript的开发能力。

« 上一篇 Vue3 + TypeScript 系列教程 - 第55集:ref与reactive的类型标注 下一篇 » Vue3 + TypeScript 系列教程 - 第57集:泛型在Vue组件中的应用