Vue Query 与 TypeScript 实战:从类型推断、类型收窄到错误类型注册

发布时间:2026/9/10 7:57:58
Vue Query 与 TypeScript 实战:从类型推断、类型收窄到错误类型注册 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 Querytanstack/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: Refnumber | Refundefined queryKey: [test], queryFn: () Promise.resolve(5), })这里有两个关键点类型被包装为Ref与 React Query 不同Vue Query 返回的是 Vue 的响应式引用因此类型层面表现为Refnumber而非裸的number。初始状态为undefined联合查询在首次成功之前没有数据因此类型是number | undefined的联合最终表现为Refnumber | Refundefined。这一点在源码中有直接体现。packages/vue-query/src/useBaseQuery.ts#L27-L40 中定义的UseBaseQueryReturnType类型明确将结果的每个属性映射为RefReadonlyTResult[K]export type UseBaseQueryReturnType TData, TError, TResult QueryObserverResultTData, TError, { [K in keyof TResult]: K extends | fetchNextPage | fetchPreviousPage | refetch ? TResult[K] : RefReadonlyTResult[K] } { suspense: () PromiseTResult }注意fetchNextPage、fetchPreviousPage、refetch这三个函数属性不会被包装成 Ref它们保持原样其余状态属性如data、error、isSuccess全部映射为Ref。二、select变换后的类型推断select选项用于在数据进入组件前做变换该变换不影响查询缓存中存储的数据。TypeScript 同样能根据select的返回值类型推导出变换后的data类型const { data } useQuery({ // ^? const data: Refstring | Refundefined queryKey: [test], queryFn: () Promise.resolve(5), select: (data) data.toString(), })queryFn返回numberselect将其转为string于是data的类型变为Refstring | Refundefined。这正是select选项的泛型签名在起作用packages/vue-query/src/useQuery.ts#L22-L65 中UseQueryOptions的类型参数TData默认等于TQueryFnData会随select的返回类型收窄data的Ref包装也随之变化。三、泛型查询函数的类型推导当queryFn是返回泛型 Promise 的具名函数时推断同样生效。例如使用 axios 请求一组Groupconst fetchGroups (): PromiseGroup[] axios.get(/groups).then((response) response.data) const { data } useQuery({ queryKey: [groups], queryFn: fetchGroups }) // ^? const data: RefGroup[] | Refundefineddata被精确推导为RefGroup[] | Refundefined无需任何手动泛型标注。这在真实项目中意味着只要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的显式守卫将RefGroup[] | Refundefined收窄为RefGroup[]随后访问.value即可获得Group[]。源码层面packages/vue-query/src/useBaseQuery.ts#L110-L114 展示了内部状态实际是reactive(observer.getCurrentResult())或shallowReactive随后在第 217 行通过toRefs(readonlyState)将响应式对象拆解为独立 refs 返回——这正是直接解构得到独立 refs这一类型事实的实现根源。五、错误类型的推断与收窄useQuery返回的error默认被推断为Refunknownconst { error } useQuery({ queryKey: [groups], queryFn: fetchGroups }) // ^? const error: Refunknown if (error.value instanceof Error) { error.value // ^? const error: Error }error的默认类型是unknown而非具体的Error这是有意设计Query Core 无法预知你的请求库会抛出什么类型的异常。因此收窄的唯一可靠方式是使用instanceof或自定义类型守卫在调用点显式判断。通过instanceof Error后error.value被精确收窄为Error。六、进阶用Register接口注册全局错误类型如果希望全项目统一的错误类型例如自定义的ApiError或更保守的unknownTanStack 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 | nullRefunknown | Refnull调用点必须显式收窄才能安全使用。该机制的源头在 Query Corepackages/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 MaybeGetterT T | (() T) export type MaybeRefT RefT | ComputedRefT | T export type MaybeRefOrGetterT MaybeRefT | (() T)而useQuery的选项类型packages/vue-query/src/useQuery.ts#L22-L65对每个选项做了精细的响应式区分queryKey接受MaybeRef即Ref、ComputedRef或普通值查询会自动追踪其中的响应式依赖并在其变化时重新请求enabled接受MaybeRefOrGetterboolean | undefined或返回QueryBooleanOption的函数可基于派生状态动态控制是否发起请求其余选项接受MaybeRefDeep支持对象/数组内部深层嵌套的响应式值。这意味着类型系统与响应式行为是严格一致的只有类型上允许传Ref的选项运行时才会被追踪。例如在自定义 composable 中应使用MaybeRefOrGetterstring作为参数类型配合toValue()解包参见 响应式指南既支持传普通字符串也支持传ref或响应式 getter。八、收尾Vue Query 类型使用清单useQuery的data总是Ref包装类型为RefTData | Refundefined直到查询成功才有确定值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),仅供参考