Vue 3 的组合式 API 与 TypeScript 深度集成,让类型推导和显式标注都变得非常自然。这一节聚焦于日常开发中最常用的两个场景:如何给响应式数据标注类型,以及 如何为自定义 Hooks 编写准确的类型签名。
响应式数据的类型标注
1. ref:自动推导 + 必要时显式泛型
ref 会根据初始值自动推导类型,大多数情况下你不需要手动标注:
import { ref } from 'vue'
const count = ref(0) // Ref<number>
const message = ref('hello') // Ref<string>
const user = ref({ name: 'Alice', age: 25 }) // Ref<{ name: string; age: number }>
当初始值无法提供完整类型信息时(例如初始值为 null,后续会赋值),使用显式泛型:
const user = ref<User | null>(null) // Ref<User | null>
// 后续赋值
user.value = { name: 'Bob', age: 30, role: 'admin' }
如果某个 ref 存储的是基础类型,不想被自动推导为更窄的字面量类型,也可以显式标注:
const status = ref<'idle' | 'loading' | 'error'>('idle')
// 这样 status.value 就是联合类型,可以被赋值为 'loading' 或 'error'
注意:ref 的类型是 Ref<T>,访问 .value 得到 T。在模板中会自动解包,但在 TypeScript 代码逻辑中必须使用 .value。
2. reactive:推荐直接为初始化对象定义接口
reactive 同样会根据传入的对象推导类型,但为了更好的可读性与代码提示,强烈建议将被 reactive 包裹的对象显式定义类型:
import { reactive } from 'vue'
interface FormState {
username: string
password: string
remember: boolean
}
const form = reactive<FormState>({
username: '',
password: '',
remember: false
})
// 得到的类型是 FormState,直接访问 form.username 即可,不需要 .value
对于复杂嵌套对象,同样可以定义完整接口或使用类型别名:
interface CartItem {
id: number
name: string
price: number
quantity: number
}
interface ShoppingCart {
items: CartItem[]
total: number
loading: boolean
}
const cart = reactive<ShoppingCart>({
items: [],
total: 0,
loading: false
})
注意:reactive 适用于对象/数组等引用类型,不适用于基础类型(基础类型必须用 ref)。这本身就是 TypeScript 类型系统的一种约束体现。
3. computed:返回值类型自动推导
计算属性的类型由其回调函数的返回值自动推导:
import { ref, computed } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2) // ComputedRef<number>
const user = ref({ firstName: 'John', lastName: 'Doe' })
const fullName = computed(() => `${user.value.firstName} ${user.value.lastName}`) // ComputedRef<string>
如果需要限制计算属性返回特定类型,也可以显式泛型:
const isValid = computed<boolean>(() => {
return form.username.length > 3 && form.password.length >= 6
})
自定义 Hooks 的类型标注
自定义 Hooks(也叫组合式函数)本质上是返回一组响应式数据或方法的普通 TypeScript 函数。为了在调用处获得良好的类型提示,必须为参数和返回值显式标注类型。
1. 函数签名设计原则
- 参数:使用明确的接口或类型别名,不要依赖隐式推导。
- 返回值:返回一个对象,标注完整的属性类型。如果需要导出多个方法且包含
ref,可以适当解构。
import { ref, computed } from 'vue'
// 定义参数类型
interface UsePaginationOptions {
pageSize?: number
total?: number
}
// 定义返回值类型
interface UsePaginationReturn {
currentPage: Ref<number>
pageSize: Ref<number>
totalPages: ComputedRef<number>
nextPage: () => void
prevPage: () => void
}
export function usePagination(options: UsePaginationOptions = {}): UsePaginationReturn {
const currentPage = ref(1)
const pageSize = ref(options.pageSize ?? 10)
const total = ref(options.total ?? 0)
const totalPages = computed(() => Math.ceil(total.value / pageSize.value))
function nextPage() {
if (currentPage.value < totalPages.value) currentPage.value++
}
function prevPage() {
if (currentPage.value > 1) currentPage.value--
}
return {
currentPage,
pageSize,
totalPages,
nextPage,
prevPage
}
}
使用时,TypeScript 会自动推断出 currentPage 是 Ref<number>,调用 nextPage 也有完整的函数签名。
2. 避免返回 ref 的包装类型丢失
当从一个 Hook 中返回多个 ref 时,不要用对象包裹的方式手动声明返回值类型为 Record<string, Ref<any>>,而应该精确声明每个字段的类型,让调用方直接拿到 Ref<T>,而不是一个泛化后的联合类型。
3. 处理可选参数和默认值
使用 TypeScript 函数参数默认值和可选标记:
interface UseFetchOptions {
immediate?: boolean
headers?: Record<string, string>
}
export function useFetch<T>(url: string, options: UseFetchOptions = {}) {
const { immediate = true, headers = {} } = options
const data = ref<T | null>(null)
// ...
}
4. 泛型 Hooks 的标注
当 Hook 需要处理不同数据类型时,使用泛型:
export function useStorage<T>(key: string, defaultValue: T) {
const stored = localStorage.getItem(key)
const data = ref<T>(stored ? JSON.parse(stored) : defaultValue)
watch(data, (newVal) => {
localStorage.setItem(key, JSON.stringify(newVal))
})
return data
}
// 使用
const theme = useStorage<'light' | 'dark'>('theme', 'light')
这里的 useStorage 会根据传入的 defaultValue 推导出 T,同时对返回值 ref 的类型做了约束。
5. 在 <script setup> 中使用组合式 Hooks
由于 <script setup> 中的顶层导入会被自动暴露给模板,如果 Hook 返回的是一个对象,需要确保其类型定义清晰,这样模板中使用时才不会丢失提示。对于返回多个独立 ref 的 Hook,可以在 <script setup> 中直接解构,TypeScript 仍然能保持正确类型:
// hooks/useMouse.ts
export function useMouse() {
const x = ref(0)
const y = ref(0)
// ...
return { x, y }
}
// 组件中
<script setup lang="ts">
import { useMouse } from './hooks/useMouse'
const { x, y } = useMouse()
// x: Ref<number>, y: Ref<number>
</script>
常见问题与最佳实践
- 不要用
any偷懒:一旦在 Hook 返回值中使用any,整个类型推导就废了。如果实在无法预先确定类型,优先使用unknown并配合类型守卫。 - 为
reactive对象使用as进行类型断言时需谨慎:这会绕过类型检查,尽量使用显式接口。 - 利用
UnwrapRef工具类型:Vue 提供了UnwrapRef<T>用于获取ref解包后的类型,可用于一些高级泛型推导场景。
响应式数据和 Hooks 的类型标注并不复杂,只要养成“每次定义 Hook 都写明参数和返回值接口”的习惯,就能让 Vue + TypeScript 的协作如鱼得水。