Vue 3 的组合式 API 在设计上考虑了 TypeScript 的类型推导,和 Options API 相比,组合式函数在类型推断上有天然优势——你不需要在 this 上声明类型,直接写 ref() 就能得到完整的类型链。
但实际项目中还是能看到类型信息断裂的情况:ref<any[]> 取代了精确的类型,defineProps 用了运行时声明而不是类型声明,provide/inject 的注入值变成了隐式的 unknown。
ref 的类型推导
Vue 3 的 ref() 会根据初始值推导类型:
import { ref, computed } from 'vue'
const count = ref(0)
// Ref<number>
const user = ref({ name: 'Alice', age: 30 })
// Ref<{ name: string; age: number }>
const list = ref([])
// Ref<never[]> —— 空数组推导为 never[]
第三个例子常见的问题是:稍后往 list 里 push 元素时会提示类型不匹配:
const list = ref([])
// list 的类型是 Ref<never[]>
// 给 list.value 赋值会出问题
// 正确做法:显式泛型
const list = ref<string[]>([])
list.value.push('hello') // ✅
// 或者用初始值
const list = ref(['hello'])
// Ref<string[]>
Ref<T> 类型不是普通的 { value: T }。它是一个特殊的泛型接口,Vue 在类型层面区分了响应式对象和普通对象。在模板或 watch 中,ref 会自动解包,但在重新赋值时需要注意:
const state = ref({
items: [] as string[],
count: 0,
})
// 正确的更新方式
state.value = {
items: ['a', 'b'],
count: 2,
}
// 单个属性更新
state.value.items.push('c')
defineProps 的类型声明
Vue 3.3+ 支持 defineProps 的纯类型声明语法,这是 Vue 3 中类型安全最大的改进之一:
// ❌ 运行时声明(类型推导弱)
const props = defineProps({
name: { type: String, required: true },
age: { type: Number, default: 0 },
tags: { type: Array as () => string[], default: () => [] },
})
// ✅ 类型声明(推荐)
const props = defineProps<{
name: string
age?: number
tags?: string[]
}>()
// ✅ 联合类型 + 泛型辅助
interface TableProps<T> {
data: T[]
columns: ColumnDef<T>[]
loading?: boolean
pagination?: { page: number; pageSize: number }
}
const props = defineProps<TableProps<User>>()
类型声明的优势:
- 不需要写
PropType<T>来推导复杂类型 - 联合类型、泛型、交叉类型都直接支持
- default 值需要在
withDefaults中声明:
const props = withDefaults(defineProps<{
name: string
age?: number
items?: string[]
}>(), {
age: 18,
items: () => [],
})
组合式函数的泛型
编写可复用的组合式函数时,泛型是保证调用方类型不丢失的关键:
// 简单组合式函数
function useToggle(initial = false) {
const state = ref(initial)
const toggle = () => { state.value = !state.value }
return { state, toggle }
}
// 调用方得到 { state: Ref<boolean>; toggle: () => void }
更复杂的场景——异步数据获取的组合式函数:
interface UseAsyncResult<T> {
data: Ref<T | null>
error: Ref<string | null>
loading: Ref<boolean>
execute: () => Promise<void>
}
function useAsync<T>(fetcher: () => Promise<T>): UseAsyncResult<T> {
const data = ref<T | null>(null) as Ref<T | null>
const error = ref<string | null>(null)
const loading = ref(false)
async function execute() {
loading.value = true
error.value = null
try {
data.value = await fetcher()
} catch (e) {
error.value = e instanceof Error ? e.message : 'Unknown error'
} finally {
loading.value = false
}
}
return { data, error, loading, execute }
}
// 使用
const { data, loading } = useAsync(() => api.getUsers())
// data: Ref<User[] | null>
// loading: Ref<boolean>
这里泛型 T 由传入的 fetcher 函数返回值自动推导。如果不用泛型,要么写成 useAsync<User[]>() 显式传参,要么丢失 data 的类型。
带参数的组合式函数
function usePolling<T>(
fetcher: () => Promise<T>,
intervalMs: number
) {
const data = ref<T | null>(null)
const error = ref<string | null>(null)
let timer: ReturnType<typeof setInterval> | null = null
async function poll() {
try {
data.value = await fetcher()
error.value = null
} catch (e) {
error.value = String(e)
}
}
function start() {
poll()
timer = setInterval(poll, intervalMs)
}
function stop() {
if (timer) { clearInterval(timer); timer = null }
}
onUnmounted(stop)
return { data, error, start, stop }
}
provide/inject 的类型链路
Vue 3 中 provide 和 inject 默认没有类型约束——inject('key') 返回 unknown。类型安全需要显式声明:
// ❌ 不安全
provide('theme', 'dark')
const theme = inject('theme') // unknown
// ✅ 使用 InjectionKey
import type { InjectionKey } from 'vue'
const ThemeKey: InjectionKey<string> = Symbol('theme')
provide(ThemeKey, 'dark')
const theme = inject(ThemeKey) // string | undefined
对于复杂类型:
interface AuthContext {
user: Ref<User | null>
login: (creds: Credentials) => Promise<void>
logout: () => void
isAuthenticated: ComputedRef<boolean>
}
const AuthKey: InjectionKey<AuthContext> = Symbol('auth')
// 在父组件中提供
const user = ref<User | null>(null)
const isAuthenticated = computed(() => user.value !== null)
provide(AuthKey, {
user,
login: async (creds) => { /* ... */ },
logout: () => { user.value = null },
isAuthenticated,
})
// 在子组件中注入
function useAuth() {
const context = inject(AuthKey)
if (!context) {
throw new Error('useAuth() must be used inside a component that provides auth context')
}
return context
}
inject 的返回值是 T | undefined——因为组件可能在没有 provide 的情况下调用 inject。这就是为什么 useAuth() 里要检查 context 是否存在。
emits 和 slots 的类型安全
emits 的类型声明
// 运行时声明
const emit = defineEmits(['update', 'delete'])
// 类型声明
const emit = defineEmits<{
(e: 'update', id: string, data: Partial<User>): void
(e: 'delete', id: string): void
}>()
emit('update', '123', { name: 'Alice' }) // ✅ 类型检查
emit('delete', '123') // ✅
emit('update', 123) // ❌ —— id 应为 string
slots 的类型声明
// 3.3+ 支持
defineSlots<{
default(props: { item: User; index: number }): any
header(): any
footer(): any
}>()
// 使用
<template>
<slot name="default" :item="user" :index="0" />
</template>
类型安全的最佳实践
// 1. 组合式函数始终返回明确的接口类型
function useFeature(): { state: Ref<State>; actions: Actions } {
return { state, actions }
}
// 2. 避免在组合式函数内部使用 any,用泛型传递类型
function useStorage<T>(key: string, initial: T): Ref<T> {
const stored = localStorage.getItem(key)
const data = ref<T>(stored ? JSON.parse(stored) : initial)
watch(data, (val) => localStorage.setItem(key, JSON.stringify(val)))
return data
}
// 3. props 优先用类型声明而非运行时声明
// 4. InjectionKey 是 provide/inject 类型安全的前提
总结
| 场景 | 类型手段 | 常见陷阱 |
|---|---|---|
| ref 类型推导 | 初始值推导或显式泛型 | 空数组推导为 never |
| defineProps | 类型声明语法 | 忘记 withDefaults 设置默认值 |
| 组合式函数 | 泛型传递调用方类型 | 内部用 any 阻断类型链 |
| provide/inject | InjectionKey | 忘记 null 检查 |
| emits | 类型声明语法 | 运行时声明不检查参数类型 |
| slots | defineSlots 类型 | 3.3 以下版本不支持 |
Vue 3 的组合式 API 在类型安全上的核心原则是:类型信息应该从调用方流向被调用方。组合式函数的类型由入参推导,provide 的类型由 key 携带,emit 的类型由声明确定。任何打破这一流向的做法(如内部强制类型断言、宽泛的 Ref<any>)都会导致类型链断裂。


