拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Angular Query 的 QueriesResults 类型:递归类型映射如何为并行查询推导精确结果类型

Angular Query 的 QueriesResults 类型递归类型映射如何为并行查询推导精确结果类型【免费下载链接】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 的 Angular Querytanstack/angular-query-experimental中injectQueries允许你在一个组件里动态并行订阅多个查询。为了让编译器能根据传入的 query 配置数组精确推导出每个查询各自的结果类型仓库在 inject-queries.ts 中实现了一个纯类型层面的递归工具——QueriesResults。本文围绕 QueriesResults.md 展开逐行拆解该递归映射类型的四个分支、支撑它的辅助类型GetCreateQueryResult、MAXIMUM_DEPTH、与CreateQueryResult/Angular Signal 体系的关系以及它在injectQueries签名与运行时中的实际作用帮助读者理解为什么写了一个queries数组返回的每个结果却拥有各自独立的类型。一、QueriesResults是谁与injectQueries的关系QueriesResults被定义并导出在 inject-queries.ts 中并在包的公共入口 index.ts 中以export type { QueriesOptions, QueriesResults } from ./inject-queries对外暴露。它与injectQueries函数的三处签名直接绑定泛型参数推导inject-queries.ts:220-226export function injectQueries T extends Arrayany, TCombinedResult QueriesResultsT, ( optionsFn: () InjectQueriesOptionsT, TCombinedResult, injector?: Injector, ): SignalTCombinedResultInjectQueriesOptions中的combine回调inject-queries.ts:204-214export interface InjectQueriesOptions T extends Arrayany, TCombinedResult QueriesResultsT, { queries: | readonly [...QueriesOptionsT] | readonly [ ...{ [K in keyof T]: GetCreateQueryOptionsForCreateQueriesT[K] }, ] combine?: (result: QueriesResultsT) TCombinedResult }运行时对结果数组元素的包装inject-queries.ts:319-328return computed(() { const result resultSignal() const { combine } optionsSignal() return combine ? result : (result as QueriesResultsT).map((query) signalProxy(signal(query))) })可见QueriesResultsT在类型层面定义了injectQueries返回值中结果数组的形态在没有提供combine时它还会被用作运行时map的类型断言依据。文档中对该类型的描述正是QueriesResults reducer recursively maps type param to results一个把类型参数递归映射为结果的 reducer。在理解它的作用之前先引入它的使用场景——Angular 框架文档中的并行查询指南export class AppComponent { users signalArrayUser([]) userQueries injectQueries(() ({ queries: users().map((user) { return { queryKey: [user, user.id], queryFn: () fetchUserById(user.id), } }), })) }当queries里的每个元素返回不同数据例如有的是User有的是Team时QueriesResults要保证userQueries()[0].data()与userQueries()[1].data()拥有不同的元素类型而不是全部退化成宽泛的联合类型或any。这正是QueriesResults递归工作的核心目标。二、类型定义逐段拆解参考文档 QueriesResults.md 收录的原始定义为type QueriesResultsT, TResults, TDepth TDepth[length] extends MAXIMUM_DEPTH ? CreateQueryResult[] : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetCreateQueryResultHead] : T extends [infer Head, ...(infer Tails)] ? QueriesResults[...Tails], [...TResults, GetCreateQueryResultHead], [...TDepth, 1] : { [K in keyof T]: GetCreateQueryResultT[K] }而仓库源码中的完整签名带有默认值参数inject-queries.ts:186-202export type QueriesResults T extends Arrayany, TResults extends Arrayany [], TDepth extends ReadonlyArraynumber [], TDepth[length] extends MAXIMUM_DEPTH ? ArrayCreateQueryResult : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetCreateQueryResultHead] : T extends [infer Head, ...infer Tails] ? QueriesResults [...Tails], [...TResults, GetCreateQueryResultHead], [...TDepth, 1] : { [K in keyof T]: GetCreateQueryResultT[K] }它采用递归 尾元组累积的写法三个泛型参数职责清晰泛型参数约束默认值含义TT extends Arrayany无传入的 query 配置元组/数组类型是唯一需要外部提供的参数TResultsT extends Arrayany[]递归过程中累积的结果类型元组每次递归把已处理完的部分结果追加到尾部TDepthT extends ReadonlyArraynumber[]递归深度计数器每展开一个元素就追加一个1用于触发深度保护2.1 四个条件分支的含义TypeScript 的条件类型conditional type在这里以extends链的形式依次尝试命中即停止深度保护分支TDepth[length] extends MAXIMUM_DEPTH ? ArrayCreateQueryResult——当累积的TDepth元组长度达到上限MAXIMUM_DEPTH 20inject-queries.ts:52时停止逐元素精确推导整体退化为ArrayCreateQueryResult。源码注释说明了动机Avoid TS depth-limit error in case of large array literal避免大型数组字面量触发 TS 递归深度上限报错。空元组分支T extends [] ? []——空数组的查询结果为[]类型保证没有查询就没有结果元素。单元素分支T extends [infer Head] ? [...TResults, GetCreateQueryResultHead]——这是递归的基础情形base case只剩最后一个 query 配置时不再继续递归而是把最后一个结果GetCreateQueryResultHead追加进累积结果元组后返回。多元素尾递归分支T extends [infer Head, ...infer Tails] ? QueriesResults[...Tails], [...TResults, GetCreateQueryResultHead], [...TDepth, 1]——每次取出元组头部Head将GetCreateQueryResultHead追加到累积结果尾部同时把TDepth加长一追加字面量1然后对剩余尾部Tails继续递归。兜底映射分支当前面的条件都不成立说明T是任意长度的数组、无法用元组模式匹配时用同态映射类型{ [K in keyof T]: GetCreateQueryResultT[K] }逐个下标映射。这一分支覆盖了运行时长度未知、由.map()动态生成的查询数组场景——此时无法推断出精确的元组形状但至少能保证数组内每个元素的类型由对应位置的 query 配置推导而来。2.2 为什么是尾递归累积而不是直接映射注意单元素分支与多元素分支都返回的是以逗号拼接的元组类型tuple type 中的 rest 展开而非直接做下标映射。这种写法有两个收益其一元组展开能保留第 K 个结果来自第 K 个 query的位置关系其二相比对任意keyof T做映射递归匹配元组可以只在元素间做模式匹配为前面的深度保护分支提供自然的计数通道——TDepth每步只加1直到 20 上限。三、两翼辅助类型GetCreateQueryResult 与 Defined 区分QueriesResults本身并不直接生成结果类型实际做单个 query 配置 → 单个结果类型翻译的是GetCreateQueryResultTinject-queries.ts:110-139。它同样用条件链处理三种写法type GetCreateQueryResultT // Part 1: 显式类型参数以对象形式给出{ queryFnData, error, data } 等 T extends { queryFnData: any; error?: infer TError; data: infer TData } ? GetDefinedOrUndefinedQueryResultT, TData, TError : T extends { queryFnData: infer TQueryFnData; error?: infer TError } ? GetDefinedOrUndefinedQueryResultT, TQueryFnData, TError : T extends { data: infer TData; error?: infer TError } ? GetDefinedOrUndefinedQueryResultT, TData, TError // Part 2: 显式类型参数以元组形式给出[TQueryFnData, TError, TData] : T extends [any, infer TError, infer TData] ? GetDefinedOrUndefinedQueryResultT, TData, TError : T extends [infer TQueryFnData, infer TError] ? GetDefinedOrUndefinedQueryResultT, TQueryFnData, TError : T extends [infer TQueryFnData] ? GetDefinedOrUndefinedQueryResultT, TQueryFnData // Part 3: 没有显式参数从 queryFn/select/throwOnError 中推断 : T extends { queryFn?: | QueryFunctioninfer TQueryFnData, any | SkipTokenForCreateQueries select?: (data: any) infer TData throwOnError?: ThrowOnErrorany, infer TError, any, any } ? GetDefinedOrUndefinedQueryResult T, unknown extends TData ? TQueryFnData : TData, unknown extends TError ? DefaultError : TError : CreateQueryResult核心的分派逻辑并不重复实现而是全部收敛到GetDefinedOrUndefinedQueryResultT, TData, TErrorinject-queries.ts:94-108这个辅助类型负责判断initialData是否确定存在type GetDefinedOrUndefinedQueryResultT, TData, TError unknown T extends { initialData?: infer TInitialData } ? unknown extends TInitialData ? CreateQueryResultTData, TError // 没有 initialData : TInitialData extends TData ? DefinedCreateQueryResultTData, TError // 对象形式且与 TData 兼容 → 已确定 : TInitialData extends () infer TInitialDataResult ? unknown extends TInitialDataResult ? CreateQueryResultTData, TError // 函数返回 undefined → 不确定 : TInitialDataResult extends TData ? DefinedCreateQueryResultTData, TError // 函数形式返回确定值 → 已确定 : CreateQueryResultTData, TError : CreateQueryResultTData, TError : CreateQueryResultTData, TError源码注释言简意赅地说明了设计意图A defined initialData setting should return a DefinedCreateQueryResult rather than CreateQueryResult设置了确定的 initialData 应返回 Defined 版本而不是 CreateQueryResult。由此可以推断出本类型体系的两层语义CreateQueryResultTData, TError意味着data可能是undefined查询可能尚未成功DefinedCreateQueryResultTData, TError意味着只要提供了与返回数据兼容的initialDatadata在第一个渲染周期即可确定存在类型上不再含undefined。这种区分直接影响消费端写法对带确定initialData的查询.data()无需判空即可使用对没有initialData的查询.data()仍须处理undefined。四、最终结果类型CreateQueryResult 与 Signal 代理QueriesResults兜底分支深度保护时直接引用CreateQueryResult单元素/映射分支则通过GetDefinedOrUndefinedQueryResult间接落到CreateQueryResult或DefinedCreateQueryResult。它们的定义位于 types.tsexport type CreateBaseQueryResult TData unknown, TError DefaultError, TState QueryObserverResultTData, TError, BaseQueryNarrowingTData, TError MapToSignalsOmitKeyofTState, keyof BaseQueryNarrowing, safely export type CreateQueryResult TData unknown, TError DefaultError, CreateBaseQueryResultTData, TError export type DefinedCreateQueryResult TData unknown, TError DefaultError, TState DefinedQueryObserverResultTData, TError, BaseQueryNarrowingTData, TError MapToSignalsOmitKeyofTState, keyof BaseQueryNarrowing, safely关键点在于MapToSignals...定义于 signal-proxy.ts它把来自tanstack/query-core的QueryObserverResult的各个字段data、status、error、isFetching等映射为Angular Signal 形态。因此在 Angular Query 中读取结果要调用queryResult.data()、queryResult.isPending()这样的方法而不是直接访问属性data带不带undefined正是CreateQueryResult与DefinedCreateQueryResult这一对类型所编码的关键区别。而BaseQueryNarrowing则把isSuccess()、isError()、isPending()实现为 TypeScript 类型守卫types.ts:51-73让这些谓词方法可以收窄状态的联合类型。综上QueriesResults展开出的每个结果元素最终都是Signal 化状态字段 谓词收窄的CreateQueryResult/DefinedCreateQueryResult组合。五、类型行为由类型测试背书仓库用vitest的expectTypeOf在编译期验证了这一类型体系的正确性测试集中在 inject-queries.test-d.tsinitialData 决定 data 是否含 undefined第 11-50 行三个 query 中前两个带对象/字符串形式的initialData其data()被断言为{ wow: boolean }与string无undefined第三个不带initialData的 query其data()被断言为string | undefined。initialData 是函数且可能返回 undefined 时第 96-115 行initialData: () undefined as { wow: boolean } | undefined时data()断言为{ wow: boolean } | undefined验证了GetDefinedOrUndefinedQueryResult对函数返回类型仍需判定的处理。动态查询混合类型第 162-194 行[...queries1List, {...Queries2.get()}]的返回值被断言为Signal[...ArrayCreateQueryResultnumber, Error, CreateQueryResultboolean, Error]——这正是QueriesResults兜底映射分支与元组展开协作的结果动态部分退化为同构数组类型静态尾部仍保留精确类型。select 改变 TData第 72-94 行与skipToken 条件分支第 143-160 行分别验证了TData经由select转换后结果类型随之变化以及条件性skipToken仍能推断出CreateQueryResultnumber, Error。这些用例说明QueriesResults并非尽力而为的宽松类型而是逐情形精确推导的强类型映射。六、与 QueriesOptions 的镜像关系QueriesResults的姊妹类型QueriesOptions位于它上方仅 40 行处inject-queries.ts:144-181文档注释对称地写着 QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param。两者组成输入输出双保险QueriesOptionsT对传入的 query 配置元组做递归展开把显式的类型参数对象或元组写法以及queryFn/select/throwOnError中推断出的类型翻译成可校验的QueryObserverOptionsForCreateQueriesinject-queries.ts:39-49从而对每个 query 参数做编译期强约束QueriesResultsT对返回的结果元组做同样的递归展开翻译成对应的CreateQueryResult/DefinedCreateQueryResult序列。两者在InjectQueriesOptions的queries属性用QueriesOptionsT约束输入和combine回调接收QueriesResultsT作为输入处汇合见 inject-queries.ts:204-214。可以推断这一对 reducer 保证配置数组有多少个元素、以何种顺序组织返回的信号数组就保持同构的元组形状从而使 TypeScript 能把下标访问、combine结果推导做到底。仓库中另一份参考文档 QueriesOptions.md 与 QueriesResults.md 同目录收录且在参考索引 index.md 中并列为两个 type-alias 条目读者可对照阅读。七、实战中的类型收益综合以上源码分析QueriesResults在 Angular Query 日常使用中带来的直接收益有三点均能从实现与测试得到印证位置即类型injectQueries返回值中[0].data()、[1].data()的类型由传入配置的第 0、1 个元素分别推导混用不同数据实体时不会互相污染见 inject-queries.test-d.ts:162-194 的动态混合用例。defined 与 undefined 可区分提供兼容的initialData时返回DefinedCreateQueryResultdata()无undefined省略冗余判空反之必须处理undefined见 inject-queries.test-d.ts:11-50。大数组有兜底超过MAXIMUM_DEPTH 20的元素或由.map()动态生成的同质数组会优雅降级为ArrayCreateQueryResult避免 TypeScript 因深层递归报 Type instantiation is excessively deep 之类的错误同时以类型守卫isSuccess()/isError()/isPending()保证基本可用性。若需在结果数组之上做派生状态可配合combine使用——InjectQueriesOptions.combine?: (result: QueriesResultsT) TCombinedResultinject-queries.ts:213会接收这份精确类型的结果元组并返回自定义聚合结果此时injectQueries的返回值类型即SignalTCombinedResult。八、延伸阅读若想继续深入本主题仓库中与QueriesResults直接相关的资料路径如下类型定义源码packages/angular-query-experimental/src/inject-queries.tsQueriesResults在第 186 行QueriesOptions在第 144 行深度上限MAXIMUM_DEPTH在第 52 行基础结果类型packages/angular-query-experimental/src/types.tsCreateQueryResult/DefinedCreateQueryResult类型级测试packages/angular-query-experimental/src/tests/inject-queries.test-d.ts功能入口导出packages/angular-query-experimental/src/index.ts使用场景指南docs/framework/angular/guides/parallel-queries.md、docs/framework/angular/guides/dependent-queries.md参考文档本文对应的 QueriesResults.md以及姊妹类型 QueriesOptions.md理解QueriesResults等于掌握了 Angular Query 在一组并行查询上类型推导的核心设计用递归把输入元组结构原样映射到输出结果元组并在深度与精度之间设置好兜底策略。这一模式同样存在于QueriesOptions两者共同构成了injectQueries从参数到返回值的完整类型契约。【免费下载链接】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),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门