Preact Query 类型中枢 QueriesOptions:useQueries 如何逐元素推导每个查询的类型
2026/9/10 11:16:34 网站建设 项目流程

Preact Query 类型中枢 QueriesOptions:useQueries 如何逐元素推导每个查询的类型

【免费下载链接】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 Query 仓库中 Preact Query 适配层的核心类型别名QueriesOptions(type-aliases/QueriesOptions.md),剖析useQueries在同时执行多个并行查询时,如何让每个查询条目的queryFn/select/throwOnError被独立且精确地推导。读完本文,你将掌握QueriesOptions的递归类型设计原理、T/TResults/TDepth三个类型参数的作用、20 元素深度上限的成因,并能对照 useQueries.ts 源码理解它在真实调用处的接线方式。

QueriesOptions 是什么:useQueries 的类型入口

在 Preact Query 中,useQueries用于一次性并发执行数量可变的一组查询。与固定数量的useQuery组合不同,它的queries参数是一个数组,数组中每个元素的形状与useQuery的选项对象基本一致(见 useQueries 函数文档)。

问题随之而来:当数组中每个查询的queryFn返回不同的数据类型、每个查询各自配置了不同的selectthrowOnError时,TypeScript 需要能逐元素地推导类型,而不是把整个数组宽泛地拍平成一个统一的选项类型。这正是QueriesOptions类型别名存在的意义。

QueriesOptions在源码中定义于 packages/preact-query/src/useQueries.ts:156,其官方注释概括得十分精准:

useQueries所接受的queries数组类型。递归地解构每一个元组元素,使每个条目的queryFn/select/throwOnError都被单独推导,上限为 20 个元素。一个不透明的数组(如unknown[])会被原样返回;而一个元素类型已知但非元组的数组,或者超过 20 个元素的元组,则回退为单一的同构(homogeneous)选项类型。

核心类型签名:一纸声明看清全部行为

QueriesOptions的签名在文档中呈现为(此处整理为多行以便阅读):

type QueriesOptions< T extends Array<any>, TResults extends Array<any> = [], TDepth extends ReadonlyArray<number> = [], > = TDepth['length'] extends MAXIMUM_DEPTH ? Array<UseQueryOptionsForUseQueries> : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetUseQueryOptionsForUseQueries<Head>] : T extends [infer Head, ...infer Tails] ? QueriesOptions< [...Tails], [...TResults, GetUseQueryOptionsForUseQueries<Head>], [...TDepth, 1] > : ReadonlyArray<unknown> extends T ? T : // 若 T 是某种数组,但 unknown[] 无法赋给它, // 说明它承载着某种已知的同构类型 —— 用于推断 Array.map() 这类场景 T extends Array< UseQueryOptionsForUseQueries< infer TQueryFnData, infer TError, infer TData, infer TQueryKey > > ? Array< UseQueryOptionsForUseQueries< TQueryFnData, TError, TData, TQueryKey > > : Array<UseQueryOptionsForUseQueries>

这是一段典型的条件类型递归。通过连续的条件判断,它把输入拆分成五种形态,逐一给出对应的展开结果。理解这段类型,等于理解了useQueries的类型安全边界。

三个类型参数:T 是入口,TResults 与 TDepth 是内部实现细节

文档以专门的## Type Parameters小节对三个泛型参数做了说明:

T

T extends any[]

调用处实际书写的queries数组的类型。它既可以是一个固定长度的字面量元组(例如[{ queryKey: ['user', 1], ... }, { queryKey: ['projects'], ... }]),也可以是ids.map(...)得到的动态数组。

TResults

TResults extends any[] = []

递归过程中累积结果的内部累加器。每处理完一个元组元素,就把该元素展开后的选项类型GetUseQueryOptionsForUseQueries<Head>追加到TResults末尾。它不是设计给调用者显式指定的,而是类型系统在递归时自增的“构建中结果”。

TDepth

TDepth extends ReadonlyArray<number> = []

内部的递归深度计数器,用于与 20 元素上限(MAXIMUM_DEPTH)比对。每一次递归调用都会向TDepth追加一个1,即[...TDepth, 1],使TDepth['length']恰好等于已处理元素的个数。文档同样强调它不应被显式设置——两个内部参数的存在,让类型别名对外只暴露一个由调用方推断出来的T

递归推导的分支拆解:每种数组形态如何被处理

QueriesOptions的主体是一连串条件分支,下面按执行顺序逐一解读。

1. 深度上限:TDepth['length'] extends MAXIMUM_DEPTH

这是整个递归的终止条件。一旦深度计数器达到 20,立即返回Array<UseQueryOptionsForUseQueries>——一个全部元素都退化为通用选项类型的普通数组。

为什么恰好是 20?源码 useQueries.ts:54-55 给出了原因:

// Avoid TS depth-limit error in case of large array literal type MAXIMUM_DEPTH = 20

这一上限是为了规避 TypeScript 在大型数组字面量上触发的类型递归深度限制错误。超过 20 个元素的元组不再逐个展开,而是回退为同构类型数组,从而保证类型检查的稳定与性能。

2. 空元组:T extends []

没有任何查询时,结果直接是空元组[]

3. 单元素元组:T extends [infer Head]

只剩最后一个元素时不再递归,把累加结果收尾:

[...TResults, GetUseQueryOptionsForUseQueries<Head>]

4. 多元素元组:T extends [infer Head, ...infer Tails]

取出头部Head,展开成选项类型并追加到TResults,随后对剩余尾部继续递归,同时让TDepth加一:

QueriesOptions<[...Tails], [...TResults, GetUseQueryOptionsForUseQueries<Head>], [...TDepth, 1]>

5. 不透明数组:ReadonlyArray<unknown> extends T ? T

如果T是无法被unknown[]覆盖的“不透明”数组(例如函数签名中只声明了unknown[]),类型系统无从得知每个元素的形状,此时原样返回T,把类型信息的选择权交还给调用方。

6. 已知元素类型的非元组数组

T不能被unknown[]赋值、但又确实是某个已知选项类型构成的数组(典型的场景是ids.map((id) => ({ queryKey, queryFn }))产出的数组),则通过条件类型反向推断出元素选项的四个泛型,返回保留这些泛型的同构数组:

Array<UseQueryOptionsForUseQueries<TQueryFnData, TError, TData, TQueryKey>>

7. 兜底回退

以上分支都无法匹配时,退化为Array<UseQueryOptionsForUseQueries>

GetUseQueryOptionsForUseQueries:单个条目究竟如何被展开

QueriesOptions逐元素调用的核心工具是GetUseQueryOptionsForUseQueries<T>(useQueries.ts:60-94)。它负责把单个查询条目的“原始写法”转换成带完整泛型的UseQueryOptionsForUseQueries。源码将其分为三个优先级,并附带注释说明:

Part 1 —— 对象形态的显式泛型参数:若条目写成{ queryFnData, error?, data }这种显式携带类型参数的对象,则按其映射:

  • { queryFnData, data }UseQueryOptionsForUseQueries<TQueryFnData, TError, TData>
  • { queryFnData, error? }UseQueryOptionsForUseQueries<TQueryFnData, TError>
  • { data, error? }UseQueryOptionsForUseQueries<unknown, TError, TData>

Part 2 —— 元组形态的显式泛型参数:若条目是[TQueryFnData, TError, TData]形状的元组,同样逐一映射(分别支持三元组、二元组、一元组)。

Part 3 —— 无显式参数时的自然推断:最常用的场景。没有显式类型参数时,从条目上的queryFn(其返回类型即TQueryFnData、其 key 即TQueryKey)、select(其返回类型即TData)、throwOnError(推断TError)中反推四个泛型:

T extends { queryFn?: | QueryFunction<infer TQueryFnData, infer TQueryKey> | SkipTokenForUseQueries select?: (data: any) => infer TData throwOnError?: ThrowOnError<any, infer TError, any, any> } ? UseQueryOptionsForUseQueries< TQueryFnData, unknown extends TError ? DefaultError : TError, unknown extends TData ? TQueryFnData : TData, TQueryKey > : UseQueryOptionsForUseQueries

其中unknown extends TData ? TQueryFnData : TData表示:如果select缺省,data的类型就等于queryFn的返回类型。推断失败的兜底,则是完全不携带具体类型的UseQueryOptionsForUseQueries

UseQueryOptionsForUseQueries:与 useQuery 选项的差异点

被逐元素产出、并在QueriesOptions各处引用的UseQueryOptionsForUseQueries定义于 useQueries.ts:42-52:

// This defines the `UseQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`. // `placeholderData` function always gets undefined passed type UseQueryOptionsForUseQueries< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, > = OmitKeyof< UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, 'placeholderData' | 'subscribed' > & { placeholderData?: TQueryFnData | QueriesPlaceholderDataFunction<TQueryFnData> }

它的语义是:useQuery选项基础上做两处适配——用OmitKeyof剔除placeholderDatasubscribed两个属性,再把placeholderData重定义为接受QueriesPlaceholderDataFunction的类型。这是因为在useQueries场景下:

  • subscribed不再按条目配置,而是useQueries顶层选项(用于控制是否订阅 query cache 的更新);
  • placeholderData的回调函数始终以previousDatapreviousQuery均为undefined的方式被调用,这与useQuery中可从缓存读取上一份数据的占位函数语义不同。

换言之,数组中的每个条目虽然“看起来和useQuery一样”,但类型层面已经被裁剪和替换过,这正是UseQueryOptionsForUseQueries独立成型的价值所在。

在 useQueries 参数处的接线

QueriesOptions并非孤立存在,它被useQueries的函数签名消费(useQueries.ts:301-331):

export function useQueries< T extends Array<any>, TCombinedResult = QueriesResults<T>, >( { queries, ...options }: { queries: | readonly [...QueriesOptions<T>] | readonly [...{ [K in keyof T]: GetUseQueryOptionsForUseQueries<T[K]> }] combine?: (result: QueriesResults<T>) => TCombinedResult subscribed?: boolean }, queryClient?: QueryClient, ): TCombinedResult

注意queries是一个联合类型,由两条路径共同覆盖全部场景:

  1. readonly [...QueriesOptions<T>]:通过...展开元组,阻止 TypeScript 在字面量数组上自动加宽(widen)而丢失逐元素信息,专门覆盖固定长度的字面量元组
  2. readonly [...{ [K in keyof T]: GetUseQueryOptionsForUseQueries<T[K]> }]:映射类型逐元素展开,覆盖ids.map(...)这类动态生成的非元组数组

此外,combine的类型参数也依赖QueriesResults<T>,而QueriesResultsQueriesOptions是镜像对应的一对——前者逐元素产出的是GetUseQueryResult<Head>(根据initialData是否定义进一步区分DefinedUseQueryResultUseQueryResult),后者逐元素产出的是查询选项。二者定义相邻(useQueries.ts:156 与 useQueries.ts:207),可对照阅读 QueriesResults 类型文档。

类型推导的实战效果

把以上机制落到使用层,就能直观感受逐元素推导带来的类型收窄。

场景一:固定数量、不同类型并存的元组

import { useQueries } from '@tanstack/preact-query' // userQuery.data: User;projectsQuery.data: Project[](已被 select 转换) const [userQuery, projectsQuery] = useQueries({ queries: [ { queryKey: ['user', 1], queryFn: () => fetchUser(1), // 返回 Promise<User> }, { queryKey: ['projects'], queryFn: () => fetchProjects(), // 返回 Promise<Project[]> select: (projects) => projects.filter((p) => p.active), }, ], })

若没有QueriesOptions的元组解构,第二个查询的data会停留在unknown或统一的联合类型;有了逐元素推导,projectsQuery.data能精确到Project[]

场景二:数量动态变化

import { useQueries } from '@tanstack/preact-query' function App({ users }: { users: Array<User> }) { // users.map 产出的数组触发第 6 分支:推断并保留同构选项的泛型 const userQueries = useQueries({ queries: users.map((user) => ({ queryKey: ['user', user.id], queryFn: () => fetchUserById(user.id), })), }) return ( <ul> {userQueries.map((query, index) => { if (query.isPending) return <li key={users[index].id}>Loading...</li> if (query.isError) return <li key={users[index].id}>Error: {query.error.message}</li> return <li key={users[index].id}>{query.data.name}</li> })} </ul> ) }

场景三:combine 将结果合并为单一值

const { data, isPending, isError } = useQueries({ queries: ids.map((id) => ({ queryKey: ['post', id], queryFn: () => fetchPost(id), })), combine: (postQueries) => ({ data: postQueries.map((query) => query.data), isPending: postQueries.some((query) => query.isPending), isError: postQueries.some((query) => query.isError), }), })

combine的入参result被声明为QueriesResults<T>,因此即便合并成单一对象返回,回调内部的每个查询结果仍保留各自精确的数据类型;useQueries最终的返回值即TCombinedResult。上述两个示例同样收录于 useQueries 函数文档 的 Examples 一节。

类型层面的已知注意点

需要提醒的是,这类“逐元素强推导”并非在所有写法下都成立。仓库在并行查询指南(React 版 parallel-queries 指南)中记录了一个 TypeScript 已知限制:内联写在useQueries查询对象上的select,无法从同一个对象的queryFn推断出自己的data入参类型,而会回退为unknown。规避方式是显式标注select的参数类型,或借助queryOptions辅助函数预先定义好查询选项。这一限制源于 TypeScript 条件类型在自引用推断上的固有限制,与QueriesOptions的递归设计相关。

延伸阅读

在 Preact Query 中,QueriesOptions并非孤例。同类递归 + 逐元素推导的类型设计还体现在:

  • QueriesResults:无combine时的返回结果类型,与QueriesOptions镜像对应;
  • SuspenseQueriesOptions / SuspenseQueriesResults:useSuspenseQueries专属的选项与结果类型。在 compat Suspense 模式下,普通并行查询会因首个查询抛出 Promise 而中断,指南推荐改用useSuspenseQueries(见 Preact 并行查询指南);
  • useQueries 函数文档:Hook 的完整参数与返回值说明。

若希望从实现层面进一步验证以上机制,可以阅读 packages/preact-query/src/useQueries.ts(类型定义位于第 42~223 行,运行时实现位于第 301 行起),其中运行时通过query-coreQueriesObserver完成观察与批量调度。仓库中还保留了 React 适配层对应的类型级测试 packages/react-query/src/tests/useQueries.test-d.tsx,可从中找到大量“应当能编译 / 应当报错”的断言样例,作为理解这套类型行为的补充参照。

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询