Angular Query 类型详解:DefinedCreateInfiniteQueryResult 与「数据必有值」的 Signal 化无限查询结果
DefinedCreateInfiniteQueryResult 是 Angular Query(@tanstack/angular-query-experimental)在**确保查询结果数据一定存在(defined)**时返回的无限查询(Infinite Query)结果类型。它以 @tanstack/query-core 的 DefinedInfiniteQueryObserverResult 为数据源,再经 MapToSignals 转换为全 Signal 化的模板可读结构。阅读完本文,你将能准确理解 injectInfiniteQuery 三种重载返回类型差异背后的类型系统设计,并能在自己的 Angular 组件中通过 initialData 获得免判空的无限查询编程体验。
类型签名:从 Query Observer 到 Angular Signal
DefinedCreateInfiniteQueryResult 定义在 types.ts,是一个纯类型别名,完整定义如下:
type DefinedCreateInfiniteQueryResult<
TData,
TError,
TDefinedInfiniteQueryObserver
> = MapToSignals<TDefinedInfiniteQueryObserver>;
它在文件中与 CreateInfiniteQueryResult 相邻出现(types.ts),两者共同构成 Angular Query 对无限查询结果的两种刻画:
// 普通版本:状态可能为 pending,data 可能尚未出现
export type CreateInfiniteQueryResult<TData = unknown, TError = DefaultError> =
BaseQueryNarrowing<TData, TError> &
MapToSignals<InfiniteQueryObserverResult<TData, TError>>
// defined 版本:data 一定有值(本类型别名所在行 L117)
export type DefinedCreateInfiniteQueryResult<
TData = unknown,
TError = DefaultError,
TDefinedInfiniteQueryObserver = DefinedInfiniteQueryObserverResult<
TData,
TError
>,
> = MapToSignals<TDefinedInfiniteQueryObserver>
可以看到两类定义共享 MapToSignals 这一转换工具,差异完全来自底层状态联合类型的选取——这正是理解本类型的第一步。
三个类型参数逐一拆解
参考文档为该类型声明了三个类型参数,均有默认值,因此日常使用 injectInfiniteQuery 时几乎不需要显式传入。
TData —— 单页数据经 InfiniteData 包装后的数据形状
- 默认值:
unknown - 语义:
InfiniteQueryObserverResult与DefinedInfiniteQueryObserverResult中的data都是InfiniteData<TData, TPageParam>形式(含pages与pageParams两个数组)。当从injectInfiniteQuery调用处传入TData = InfiniteData<TQueryFnData>(这是重载的默认参数,见 inject-infinite-query.ts)时,最终结果里的data泛型即为累积全部页面的InfiniteData。
TError —— 错误类型
- 默认值:
DefaultError,即 query-core 导出的{ [key: string]: any }兜底错误对象,项目通过泛型实际返回HttpErrorResponse(Angular HttpClient)等业务错误类型时,会顶替该默认值。
TDefinedInfiniteQueryObserver —— 定义底层观察者结果形态
- 默认值:
DefinedInfiniteQueryObserverResult<TData, TError> - 语义:它承载真正的无限查询状态结构。只要不给此参数,该类型就会自动引用 query-core 的 defined 观察者结果,也就是说开发者通常根本不会感知到第三个参数的存在,却自动获得了「data 必有值」的类型保证。
关键前提:为何 DefinedInfiniteQueryObserverResult 的 data 一定有值
“Defined”不是营销词汇,而是 query-core 类型系统中的一种精确状态划分。查看 query-core/src/types.ts:
export type DefinedInfiniteQueryObserverResult<TData = unknown, TError = DefaultError> =
| InfiniteQueryObserverRefetchErrorResult<TData, TError>
| InfiniteQueryObserverSuccessResult<TData, TError>
export type InfiniteQueryObserverResult<TData = unknown, TError = DefaultError> =
| DefinedInfiniteQueryObserverResult<TData, TError>
| InfiniteQueryObserverLoadingErrorResult<TData, TError>
| InfiniteQueryObserverLoadingResult<TData, TError>
两者中 InfiniteQueryObserverSuccessResult 与 InfiniteQueryObserverRefetchErrorResult(首次已成功、后续刷新失败仍保留旧数据)都声明了非空字段(data: TData、isError: false 等,见 types.ts);被排除在外的 LoadingErrorResult 与 LoadingResult 恰恰是“查询还未拿到任何数据”的状态。
因此,从源码结构可以推断:只要结果类型是 Defined 系,就等价于保证无限查询已经从缓存或网络中拿到了一页以上数据,模板中不再需要为“data 不存在”的初载场景兜底。
MapToSignals:把 Plain Object 结果改造成 Signal 集合
DefinedCreateInfiniteQueryResult 之所以能被 Angular 模板响应式读取,核心转换器是 MapToSignals(signal-proxy.ts):
export type MapToSignals<T> = {
[K in keyof T]: T[K] extends Function ? T[K] : Signal<T[K]>
}
两个要点需要展开:
- 属性 → 信号:所有值为普通数据的字段被映射为
Signal<T[K]>,因此组件里访问query.data()、query.hasNextPage()、query.isPending()、query.fetchStatus()等都是调用信号读取最新值,Angular 变更检测会自动订阅; - 方法 → 原样透传:凡属性值是函数(例如
fetchNextPage、fetchPreviousPage、refetch、remove),映射后仍保持函数类型,模板中可直接以query.fetchNextPage()形式调用。
这种运行期行为由 signalProxy 落地(signal-proxy.ts):其内部用 Proxy 拦截属性读取,首次读取为对象字段惰性创建 computed 并缓存,若字段是函数则直接返回原函数,从而避免每帧重复包装;createBaseQuery 在末尾正是 return signalProxy(computed(...))(create-base-query.ts),将 Observer 订阅结果与乐观结果合流后整体信号化。
谁在返回该类型:injectInfiniteQuery 的重载契约
该类型唯一的实际消费方是 injectInfiniteQuery 函数的第一个重载(inject-infinite-query.ts):
export function injectInfiniteQuery<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
>(
injectInfiniteQueryFn: () => DefinedInitialDataInfiniteOptions<
TQueryFnData, TError, TData, TQueryKey, TPageParam
>,
options?: InjectInfiniteQueryOptions,
): DefinedCreateInfiniteQueryResult<TData, TError>
与之配套,DefinedInitialDataInfiniteOptions 要求 initialData 字段是非 undefined 的 InfiniteData 或返回该值的函数(infinite-query-options.ts),也就是运行时数据来源有保证,类型系统因此敢把结果收窄为 Defined 系:
type DefinedInitialDataInfiniteOptions<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData<TQueryFnData>,
...
> = CreateInfiniteQueryOptions<TQueryFnData, TError, TData, ...> & {
initialData:
| NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
| (() => NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>)
| undefined
}
而同一文件中的第二、第三个重载(接收 UndefinedInitialDataInfiniteOptions / 普通 CreateInfiniteQueryOptions)返回的是普通 CreateInfiniteQueryResult<TData, TError>(inject-infinite-query.ts)。两条契约之间的切换对调用方是完全透明、由 TS 重载自动完成的:提供 initialData,返回值自然“升级”为 Defined 类型。
实战示例:initialData 下的免判空无限查询
以下用法改编自官方指南 infinite-queries.md 的 Angular 示例,关键在于 initialData 的使用会让组件自动获得 Defined 类型:
import { Component, computed, inject } from '@angular/core'
import {
injectInfiniteQuery,
infiniteQueryOptions,
} from '@tanstack/angular-query-experimental'
import { lastValueFrom } from 'rxjs'
import { ProjectsService } from './projects-service'
@Component({
selector: 'example',
templateUrl: './example.component.html',
})
export class Example {
projectsService = inject(ProjectsService)
// initialData 就位 → 返回类型是 DefinedCreateInfiniteQueryResult
query = injectInfiniteQuery(() => ({
queryKey: ['projects'],
queryFn: async ({ pageParam }) =>
lastValueFrom(this.projectsService.getProjects(pageParam)),
initialPageParam: 0,
initialData: {
pages: [{ previousId: null, nextId: 0, data: [] }],
pageParams: [0],
},
getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined,
getNextPageParam: (lastPage) => lastPage.nextId ?? undefined,
}))
#hasNextPage = this.query.hasNextPage // Signal<boolean>
#isFetchingNextPage = this.query.isFetchingNextPage
}
@for (page of query.data().pages; track $index) {
@for (project of page.data; track project.id) {
<p>{{ project.name }} {{ project.id }}</p>
}
}
<div>
<button (click)="query.fetchNextPage()" [disabled]="!query.hasNextPage()">
Load more
</button>
</div>
值得注意的编码细节:
- 因为返回类型是 Defined,
query.data()的类型是InfiniteData<TQueryFnData>而非“可能 undefined”,模板中可以直接解引用.pages; hasNextPage、isFetchingNextPage字段经MapToSignals变成信号,类字段#hasNextPage实际存的是信号,模板中调用需写作#hasNextPage();fetchNextPage等函数被透传为普通方法,因此(click)="query.fetchNextPage()"直接可执行。
若去掉 initialData,同样的组件返回普通 CreateInfiniteQueryResult,此时 query.data() 可能为 undefined,官方示例中就需要用 @if (query.isPending()) 先做分支渲染(infinite-queries.md)。
与相邻结果类型的横向对比
为了让读者在重载签名和类型收窄之间不迷路,下表按 types.ts 归纳:
| 类型 | 底层状态 | 结构差异 | 数据是否必有 |
|---|---|---|---|
CreateBaseQueryResult |
QueryObserverResult |
交叉 BaseQueryNarrowing(isSuccess 等守卫,见 types.ts) |
否 |
CreateQueryResult |
QueryObserverResult |
同上(查询专用别名) | 否 |
DefinedCreateQueryResult |
DefinedQueryObserverResult |
同上但状态为 Defined 联合 | 是(普通查询) |
CreateInfiniteQueryResult |
InfiniteQueryObserverResult |
交叉 BaseQueryNarrowing + MapToSignals |
否(首次加载可能无 data) |
DefinedCreateInfiniteQueryResult |
DefinedInfiniteQueryObserverResult |
仅 MapToSignals,无收窄守卫 |
是(无限查询) |
一个容易忽略的点:CreateInfiniteQueryResult 交叉了 BaseQueryNarrowing,用 query.isSuccess() 等方式做类型守卫;而 DefinedCreateInfiniteQueryResult 不再需要守卫——因为联合类型里根本不存在“无 data”的分支,MapToSignals 直接整体转换即得到安全结果。
使用建议与边界说明
- 优先让类型自动推导:不要手写三个类型参数,让
injectInfiniteQuery(() => ({ ..., initialData }))的重载推导完成收窄;infiniteQueryOptions(infinite-query-options.ts)可把 Defined 选项在模块间以类型安全的方式共享复用。 - 理解“Defined”≠“永远在请求”:Defined 只承诺
data有值(含刷新失败后保留的陈旧数据),不承诺查询空闲或正在后台刷新——isFetching、fetchStatus仍需按业务需要读取,这与DefinedInfiniteQueryObserverResult中RefetchError分支的含义一致。 - 模板与类型脱钩:
MapToSignals把普通字段变信号、函数留原样的契约决定了:组件类字段若直接保存query.data,得到的是一等公民Signal,赋值、比较都要用()调用;一旦有 “Defined” 保证,data()解引用不会在模板编译层报可空错。 - 参考类型在官方 API 文档索引见 reference/type-aliases/DefinedCreateInfiniteQueryResult.md,其同名入口函数说明见 reference/functions/injectInfiniteQuery.md。
综上,DefinedCreateInfiniteQueryResult 可被概括为一句话:Angular Query 用 initialData 向类型系统证明无限查询必有数据,再用 MapToSignals 把它翻译成模板友好的 Signal 结构,最终交出“免判空、可响应、方法直调”的组件侧查询句柄。理解它的构成,也就同时理解了 Angular Query 在结果类型上“普通 / Defined”双轨设计的取舍逻辑。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00