ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

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

Preact Query 类型中枢 QueriesOptions:useQueries 如何逐元素推导每个查询的类型 Preact Query 类型中枢 QueriesOptionsuseQueries 如何逐元素推导每个查询的类型【免费下载链接】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 适配层的核心类型别名QueriesOptionstype-aliases/QueriesOptions.md剖析useQueries在同时执行多个并行查询时如何让每个查询条目的queryFn/select/throwOnError被独立且精确地推导。读完本文你将掌握QueriesOptions的递归类型设计原理、T/TResults/TDepth三个类型参数的作用、20 元素深度上限的成因并能对照 useQueries.ts 源码理解它在真实调用处的接线方式。QueriesOptions 是什么useQueries 的类型入口在 Preact Query 中useQueries用于一次性并发执行数量可变的一组查询。与固定数量的useQuery组合不同它的queries参数是一个数组数组中每个元素的形状与useQuery的选项对象基本一致见 useQueries 函数文档。问题随之而来当数组中每个查询的queryFn返回不同的数据类型、每个查询各自配置了不同的select或throwOnError时TypeScript 需要能逐元素地推导类型而不是把整个数组宽泛地拍平成一个统一的选项类型。这正是QueriesOptions类型别名存在的意义。QueriesOptions在源码中定义于 packages/preact-query/src/useQueries.ts:156其官方注释概括得十分精准useQueries所接受的queries数组类型。递归地解构每一个元组元素使每个条目的queryFn/select/throwOnError都被单独推导上限为 20 个元素。一个不透明的数组如unknown[]会被原样返回而一个元素类型已知但非元组的数组或者超过 20 个元素的元组则回退为单一的同构homogeneous选项类型。核心类型签名一纸声明看清全部行为QueriesOptions的签名在文档中呈现为此处整理为多行以便阅读type QueriesOptions T extends Arrayany, TResults extends Arrayany [], TDepth extends ReadonlyArraynumber [], TDepth[length] extends MAXIMUM_DEPTH ? ArrayUseQueryOptionsForUseQueries : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetUseQueryOptionsForUseQueriesHead] : T extends [infer Head, ...infer Tails] ? QueriesOptions [...Tails], [...TResults, GetUseQueryOptionsForUseQueriesHead], [...TDepth, 1] : ReadonlyArrayunknown 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 : ArrayUseQueryOptionsForUseQueries这是一段典型的条件类型递归。通过连续的条件判断它把输入拆分成五种形态逐一给出对应的展开结果。理解这段类型等于理解了useQueries的类型安全边界。三个类型参数T 是入口TResults 与 TDepth 是内部实现细节文档以专门的## Type Parameters小节对三个泛型参数做了说明TT extends any[]调用处实际书写的queries数组的类型。它既可以是一个固定长度的字面量元组例如[{ queryKey: [user, 1], ... }, { queryKey: [projects], ... }]也可以是ids.map(...)得到的动态数组。TResultsTResults extends any[] []递归过程中累积结果的内部累加器。每处理完一个元组元素就把该元素展开后的选项类型GetUseQueryOptionsForUseQueriesHead追加到TResults末尾。它不是设计给调用者显式指定的而是类型系统在递归时自增的“构建中结果”。TDepthTDepth extends ReadonlyArraynumber []内部的递归深度计数器用于与 20 元素上限MAXIMUM_DEPTH比对。每一次递归调用都会向TDepth追加一个1即[...TDepth, 1]使TDepth[length]恰好等于已处理元素的个数。文档同样强调它不应被显式设置——两个内部参数的存在让类型别名对外只暴露一个由调用方推断出来的T。递归推导的分支拆解每种数组形态如何被处理QueriesOptions的主体是一连串条件分支下面按执行顺序逐一解读。1. 深度上限TDepth[length] extends MAXIMUM_DEPTH这是整个递归的终止条件。一旦深度计数器达到 20立即返回ArrayUseQueryOptionsForUseQueries——一个全部元素都退化为通用选项类型的普通数组。为什么恰好是 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, GetUseQueryOptionsForUseQueriesHead]4. 多元素元组T extends [infer Head, ...infer Tails]取出头部Head展开成选项类型并追加到TResults随后对剩余尾部继续递归同时让TDepth加一QueriesOptions[...Tails], [...TResults, GetUseQueryOptionsForUseQueriesHead], [...TDepth, 1]5. 不透明数组ReadonlyArrayunknown extends T ? T如果T是无法被unknown[]覆盖的“不透明”数组例如函数签名中只声明了unknown[]类型系统无从得知每个元素的形状此时原样返回T把类型信息的选择权交还给调用方。6. 已知元素类型的非元组数组若T不能被unknown[]赋值、但又确实是某个已知选项类型构成的数组典型的场景是ids.map((id) ({ queryKey, queryFn }))产出的数组则通过条件类型反向推断出元素选项的四个泛型返回保留这些泛型的同构数组ArrayUseQueryOptionsForUseQueriesTQueryFnData, TError, TData, TQueryKey7. 兜底回退以上分支都无法匹配时退化为ArrayUseQueryOptionsForUseQueries。GetUseQueryOptionsForUseQueries单个条目究竟如何被展开QueriesOptions逐元素调用的核心工具是GetUseQueryOptionsForUseQueriesTuseQueries.ts:60-94。它负责把单个查询条目的“原始写法”转换成带完整泛型的UseQueryOptionsForUseQueries。源码将其分为三个优先级并附带注释说明Part 1 —— 对象形态的显式泛型参数若条目写成{ queryFnData, error?, data }这种显式携带类型参数的对象则按其映射{ queryFnData, data }→UseQueryOptionsForUseQueriesTQueryFnData, TError, TData{ queryFnData, error? }→UseQueryOptionsForUseQueriesTQueryFnData, TError{ data, error? }→UseQueryOptionsForUseQueriesunknown, TError, TDataPart 2 —— 元组形态的显式泛型参数若条目是[TQueryFnData, TError, TData]形状的元组同样逐一映射分别支持三元组、二元组、一元组。Part 3 —— 无显式参数时的自然推断最常用的场景。没有显式类型参数时从条目上的queryFn其返回类型即TQueryFnData、其 key 即TQueryKey、select其返回类型即TData、throwOnError推断TError中反推四个泛型T extends { queryFn?: | QueryFunctioninfer TQueryFnData, infer TQueryKey | SkipTokenForUseQueries select?: (data: any) infer TData throwOnError?: ThrowOnErrorany, 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 UseQueryOptionsTQueryFnData, TError, TData, TQueryKey, placeholderData | subscribed { placeholderData?: TQueryFnData | QueriesPlaceholderDataFunctionTQueryFnData }它的语义是在useQuery选项基础上做两处适配——用OmitKeyof剔除placeholderData与subscribed两个属性再把placeholderData重定义为接受QueriesPlaceholderDataFunction的类型。这是因为在useQueries场景下subscribed不再按条目配置而是useQueries的顶层选项用于控制是否订阅 query cache 的更新placeholderData的回调函数始终以previousData与previousQuery均为undefined的方式被调用这与useQuery中可从缓存读取上一份数据的占位函数语义不同。换言之数组中的每个条目虽然“看起来和useQuery一样”但类型层面已经被裁剪和替换过这正是UseQueryOptionsForUseQueries独立成型的价值所在。在 useQueries 参数处的接线QueriesOptions并非孤立存在它被useQueries的函数签名消费useQueries.ts:301-331export function useQueries T extends Arrayany, TCombinedResult QueriesResultsT, ( { queries, ...options }: { queries: | readonly [...QueriesOptionsT] | readonly [...{ [K in keyof T]: GetUseQueryOptionsForUseQueriesT[K] }] combine?: (result: QueriesResultsT) TCombinedResult subscribed?: boolean }, queryClient?: QueryClient, ): TCombinedResult注意queries是一个联合类型由两条路径共同覆盖全部场景readonly [...QueriesOptionsT]通过...展开元组阻止 TypeScript 在字面量数组上自动加宽widen而丢失逐元素信息专门覆盖固定长度的字面量元组readonly [...{ [K in keyof T]: GetUseQueryOptionsForUseQueriesT[K] }]映射类型逐元素展开覆盖ids.map(...)这类动态生成的非元组数组。此外combine的类型参数也依赖QueriesResultsT而QueriesResults与QueriesOptions是镜像对应的一对——前者逐元素产出的是GetUseQueryResultHead根据initialData是否定义进一步区分DefinedUseQueryResult与UseQueryResult后者逐元素产出的是查询选项。二者定义相邻useQueries.ts:156 与 useQueries.ts:207可对照阅读 QueriesResults 类型文档。类型推导的实战效果把以上机制落到使用层就能直观感受逐元素推导带来的类型收窄。场景一固定数量、不同类型并存的元组import { useQueries } from tanstack/preact-query // userQuery.data: UserprojectsQuery.data: Project[]已被 select 转换 const [userQuery, projectsQuery] useQueries({ queries: [ { queryKey: [user, 1], queryFn: () fetchUser(1), // 返回 PromiseUser }, { queryKey: [projects], queryFn: () fetchProjects(), // 返回 PromiseProject[] select: (projects) projects.filter((p) p.active), }, ], })若没有QueriesOptions的元组解构第二个查询的data会停留在unknown或统一的联合类型有了逐元素推导projectsQuery.data能精确到Project[]。场景二数量动态变化import { useQueries } from tanstack/preact-query function App({ users }: { users: ArrayUser }) { // 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被声明为QueriesResultsT因此即便合并成单一对象返回回调内部的每个查询结果仍保留各自精确的数据类型useQueries最终的返回值即TCombinedResult。上述两个示例同样收录于 useQueries 函数文档 的 Examples 一节。类型层面的已知注意点需要提醒的是这类“逐元素强推导”并非在所有写法下都成立。仓库在并行查询指南React 版 parallel-queries 指南中记录了一个 TypeScript 已知限制内联写在useQueries查询对象上的select无法从同一个对象的queryFn推断出自己的data入参类型而会回退为unknown。规避方式是显式标注select的参数类型或借助queryOptions辅助函数预先定义好查询选项。这一限制源于 TypeScript 条件类型在自引用推断上的固有限制与QueriesOptions的递归设计相关。延伸阅读在 Preact Query 中QueriesOptions并非孤例。同类递归 逐元素推导的类型设计还体现在QueriesResults无combine时的返回结果类型与QueriesOptions镜像对应SuspenseQueriesOptions / SuspenseQueriesResultsuseSuspenseQueries专属的选项与结果类型。在 compat Suspense 模式下普通并行查询会因首个查询抛出 Promise 而中断指南推荐改用useSuspenseQueries见 Preact 并行查询指南useQueries 函数文档Hook 的完整参数与返回值说明。若希望从实现层面进一步验证以上机制可以阅读 packages/preact-query/src/useQueries.ts类型定义位于第 42223 行运行时实现位于第 301 行起其中运行时通过query-core的QueriesObserver完成观察与批量调度。仓库中还保留了 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),仅供参考
返回列表