Vue Query 与 TypeScript 实战:从类型推断、类型收窄到错误类型注册
【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query
本指南聚焦 TanStack Vue Query(@tanstack/vue-query)在 TypeScript 项目中的完整类型体系:useQuery返回值的类型推断规则、select变换后的类型变化、如何正确地收窄data与error的可空类型,以及通过Register接口注册全局错误类型的进阶玩法。读完本文,你将能够在 Vue 3 + TypeScript 项目中写出类型安全、零any的数据请求代码。文中所有结论均以仓库源码(packages/vue-query/src 与 packages/query-core/src)为佐证。
一、开箱即用的类型推断:useQuery的返回值类型
在 Vue Query 中,useQuery的data返回值天然被包装为 Vue 的响应式引用(Ref)。即使不写任何显式泛型参数,TypeScript 也能从queryFn的返回类型中精确推导出data的类型:
const { data } = useQuery({ // ^? const data: Ref<number> | Ref<undefined> queryKey: ['test'], queryFn: () => Promise.resolve(5), })这里有两个关键点:
- 类型被包装为
Ref:与 React Query 不同,Vue Query 返回的是 Vue 的响应式引用,因此类型层面表现为Ref<number>而非裸的number。 - 初始状态为
undefined联合:查询在首次成功之前没有数据,因此类型是number | undefined的联合,最终表现为Ref<number> | Ref<undefined>。
这一点在源码中有直接体现。packages/vue-query/src/useBaseQuery.ts#L27-L40 中定义的UseBaseQueryReturnType类型明确将结果的每个属性映射为Ref<Readonly<TResult>[K]>:
export type UseBaseQueryReturnType< TData, TError, TResult = QueryObserverResult<TData, TError>, > = { [K in keyof TResult]: K extends | 'fetchNextPage' | 'fetchPreviousPage' | 'refetch' ? TResult[K] : Ref<Readonly<TResult>[K]> } & { suspense: () => Promise<TResult> }注意fetchNextPage、fetchPreviousPage、refetch这三个函数属性不会被包装成 Ref,它们保持原样;其余状态属性(如data、error、isSuccess)全部映射为Ref。
二、select变换后的类型推断
select选项用于在数据进入组件前做变换(该变换不影响查询缓存中存储的数据)。TypeScript 同样能根据select的返回值类型推导出变换后的data类型:
const { data } = useQuery({ // ^? const data: Ref<string> | Ref<undefined> queryKey: ['test'], queryFn: () => Promise.resolve(5), select: (data) => data.toString(), })queryFn返回number,select将其转为string,于是data的类型变为Ref<string> | Ref<undefined>。这正是select选项的泛型签名在起作用:packages/vue-query/src/useQuery.ts#L22-L65 中UseQueryOptions的类型参数TData(默认等于TQueryFnData)会随select的返回类型收窄,data的Ref包装也随之变化。
三、泛型查询函数的类型推导
当queryFn是返回泛型 Promise 的具名函数时,推断同样生效。例如使用 axios 请求一组Group:
const fetchGroups = (): Promise<Group[]> => axios.get('/groups').then((response) => response.data) const { data } = useQuery({ queryKey: ['groups'], queryFn: fetchGroups }) // ^? const data: Ref<Group[]> | Ref<undefined>data被精确推导为Ref<Group[]> | Ref<undefined>,无需任何手动泛型标注。这在真实项目中意味着:只要queryFn有明确的返回类型注解,整个查询链路的类型就是自洽的。
四、类型收窄:reactive()与直接解构的本质区别
这是 Vue Query 类型系统中最容易踩坑、也最值得深入理解的部分。先看官方推荐做法——用reactive()包装查询结果:
const { data, isSuccess } = reactive( useQuery({ queryKey: ['test'], queryFn: () => Promise.resolve(5), }), ) if (isSuccess) { data // ^? const data: number }为什么reactive()能实现跨属性收窄?
reactive()包装会把查询结果拍平为普通值(unwrapped),于是isSuccess和data成为同一个可辨识联合(discriminated union)的成员。TypeScript 可以通过isSuccess这个判别属性将data收窄为number。
为什么直接解构不行?
如果直接解构useQuery():
const { data, isSuccess } = useQuery({ queryKey: ['test'], queryFn: () => Promise.resolve(5), })此时data和isSuccess是彼此独立的 ref。TypeScript 无法把一个 ref 上的收窄结论"搬运"到另一个 ref 上——if (isSuccess.value)成立后,data.value仍然保持Group[] | undefined,不会自动收窄。这是因为在类型系统中,两个独立Ref之间不存在判别联合关系。
不用reactive()时的替代方案:收窄 value 本身
如果不使用reactive()包装,则需要直接对data.value做空值判断:
const { data } = useQuery({ queryKey: ['groups'], queryFn: fetchGroups }) if (data.value !== undefined) { data.value // ^? const data: Group[] }通过data.value !== undefined的显式守卫,将Ref<Group[]> | Ref<undefined>收窄为Ref<Group[]>,随后访问.value即可获得Group[]。
源码层面,packages/vue-query/src/useBaseQuery.ts#L110-L114 展示了内部状态实际是reactive(observer.getCurrentResult())(或shallowReactive),随后在第 217 行通过toRefs(readonlyState)将响应式对象拆解为独立 refs 返回——这正是"直接解构得到独立 refs"这一类型事实的实现根源。
五、错误类型的推断与收窄
useQuery返回的error默认被推断为Ref<unknown>:
const { error } = useQuery({ queryKey: ['groups'], queryFn: fetchGroups }) // ^? const error: Ref<unknown> if (error.value instanceof Error) { error.value // ^? const error: Error }error的默认类型是unknown而非具体的Error,这是有意设计:Query Core 无法预知你的请求库会抛出什么类型的异常。因此收窄的唯一可靠方式是使用instanceof(或自定义类型守卫)在调用点显式判断。通过instanceof Error后,error.value被精确收窄为Error。
六、进阶:用Register接口注册全局错误类型
如果希望全项目统一的错误类型(例如自定义的ApiError或更保守的unknown),TanStack Query 提供了Register接口的模块扩充机制。
将defaultError注册为unknown
默认情况下error是unknown联合null。如果你希望类型层面更保守、强制所有调用点显式收窄,可以这样注册:
import '@tanstack/vue-query' declare module '@tanstack/vue-query' { interface Register { // Use unknown so call sites must narrow explicitly. defaultError: unknown } } const { error } = useQuery({ queryKey: ['groups'], queryFn: fetchGroups }) // ^? const error: unknown | null注册后,所有useQuery/useMutation的error类型都会变为unknown | null(Ref<unknown> | Ref<null>),调用点必须显式收窄才能安全使用。
该机制的源头在 Query Core:packages/query-core/src/types.ts#L37-L46 中定义了Register接口及其defaultError类型推断:
export interface Register { // defaultError: Error ... defaultError: infer TError }TError通过Register['defaultError']提取,成为QueryObserverOptions、MutationObserverOptions等所有观察器选项的默认错误类型参数。因此 Vue Query 侧只需import '@tanstack/vue-query'并对同名Register接口做declare module扩充,即可让全局类型生效。
七、与响应式类型系统的衔接:MaybeRef与MaybeRefOrGetter
理解类型推断后,还需要理解 Vue Query 选项参数的类型设计——这也是其类型系统与 React Query 最大的不同。在 packages/vue-query/src/types.ts#L25-L29 中定义了三个核心类型:
export type MaybeGetter<T> = T | (() => T) export type MaybeRef<T> = Ref<T> | ComputedRef<T> | T export type MaybeRefOrGetter<T> = MaybeRef<T> | (() => T)而useQuery的选项类型(packages/vue-query/src/useQuery.ts#L22-L65)对每个选项做了精细的响应式区分:
queryKey:接受MaybeRef,即Ref、ComputedRef或普通值,查询会自动追踪其中的响应式依赖并在其变化时重新请求;enabled:接受MaybeRefOrGetter<boolean | undefined>或返回QueryBooleanOption的函数,可基于派生状态动态控制是否发起请求;- 其余选项:接受
MaybeRefDeep,支持对象/数组内部深层嵌套的响应式值。
这意味着类型系统与响应式行为是严格一致的:只有类型上允许传Ref的选项,运行时才会被追踪。例如在自定义 composable 中,应使用MaybeRefOrGetter<string>作为参数类型,配合toValue()解包(参见 响应式指南),既支持传普通字符串,也支持传ref或响应式 getter。
八、收尾:Vue Query 类型使用清单
useQuery的data总是Ref包装,类型为Ref<TData> | Ref<undefined>,直到查询成功才有确定值;select会改变data的类型,queryFn的返回类型注解是推断的根基;- 需要跨属性收窄(如
isSuccess收窄data)时,用reactive()包装查询结果;否则直接对data.value做显式空值判断; error默认是unknown,用instanceof收窄;团队有统一错误类型时,通过declare module '@tanstack/vue-query'扩充Register['defaultError']全局生效;- 选项类型中
queryKey接受MaybeRef、enabled接受MaybeRefOrGetter、其余接受MaybeRefDeep——类型即文档,传参时顺应类型即可获得正确的响应式行为。
更多选项细节可查阅 useQuery 参考文档,响应式取值约定可参考 Reactivity 指南。
【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考