深入解析 Angular Query 的 `DefinedInitialDataInfiniteOptions`:用类型系统驾驭带初始数据的无限查询
导读
在 TanStack Query 的 Angular 适配层(angular-query-experimental 包)中,无限查询(infinite query)的选项对象被细分为多个类型别名,以换取更强的前端类型收窄能力。DefinedInitialDataInfiniteOptions 正是其中专门描述"已提供初始数据(initialData)"场景的类型:它把 initialData 变为对象字面量必填属性,从而让 injectInfiniteQuery 能够据此返回 data 永不为 undefined 的已定义结果类型。读完本文,你将理解该类型的完整签名、每个泛型参数与属性的确切含义,掌握它与兄弟类型(UndefinedInitialDataInfiniteOptions)的分工机制,并能在 Angular 项目中正确地用缓存数据预填充无限查询。
本文关联文档为 DefinedInitialDataInfiniteOptions 类型别名文档,源码实现位于 infinite-query-options.ts。
一、类型签名总览:一个 "交叉 + 收紧" 的选项类型
关联文档给出的核心声明如下:
type DefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> =
CreateInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & object;
将文档页签去格式化后,实际展开源码定义(infinite-query-options.ts:62-79)为:
export type DefinedInitialDataInfiniteOptions<
TQueryFnData,
TError = DefaultError,
TData = InfiniteData<TQueryFnData>,
TQueryKey extends QueryKey = QueryKey,
TPageParam = unknown,
> = CreateInfiniteQueryOptions<
TQueryFnData,
TError,
TData,
TQueryKey,
TPageParam
> & {
initialData:
| NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
| (() => NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>)
| undefined
}
可以拆解为两层含义:
-
继承全部无限查询选项:
CreateInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>是 Angular Query 对 core 层InfiniteQueryObserverOptions的轻量封装。查看 types.ts:75-90 可知,它仅通过OmitKeyof剔除掉了suspense字段,其余(queryKey、queryFn、getNextPageParam、initialPageParam、maxPages、gcTime、staleTime等)全部沿用 core 语义。 -
在继承之上"收紧"了
initialData:交叉(&)出一个只含单一成员initialData的对象,且该成员被声明为必填键(注意:initialData后没有?)。这正是与兄弟类型UndefinedInitialDataInfiniteOptions(infinite-query-options.ts:13-32,其中initialData?为可选)的关键差异。
值得说明:Angular Query 中该类型的定义方式与文档页展示略有差异——类型参数列表里
TError、TData、TQueryKey、TPageParam在源码中都有默认值与约束,文档页属于生成式的 API 摘要,阅读时以源码为准。
二、initialData 属性:静态值、惰性函数与 undefined 三选一
initialData 是理解本类型的核心。其类型为:
initialData:
| NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
| (() => NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>)
| undefined;
2.1 数据形态必须是 InfiniteData
无限查询的数据不是单页对象,而是多页累加的结果。core 层 types.ts:210-213 明确定义了 InfiniteData:
export interface InfiniteData<TData, TPageParam = unknown> {
pages: Array<TData> // 已累积的所有页数据
pageParams: Array<TPageParam> // 与 pages 一一对应的每页参数
}
因此这里的 initialData 也必须是一个完整的 InfiniteData 结构(既有 pages 又有 pageParams),而不是单页裸数据。当你把本地持久化缓存或其他来源的数据写回无限查询时,必须保证二者成对出现。
2.2 NonUndefinedGuard:在类型层面排除 undefined
NonUndefinedGuard<T> 定义在 core 层 types.ts:12:
export type NonUndefinedGuard<T> = T extends undefined ? never : T
它把"恰好是 undefined"的类型映射为 never。其效果是:当你传入具体值或惰性函数时,类型系统会拒绝一个结果可能为 undefined 的初始化器。注意此收窄与 core 层宽泛的 InitialDataFunction<T> = () => T | undefined(types.ts:173)形成对比——core 允许初始数据函数返回 undefined,而 Angular Query 的 "Defined" 变体在语义上要求初始数据必须是确实存在的。
2.3 为什么联合类型里仍保留 undefined?
一个常见的困惑:既然叫 "Defined",为什么联合里还有一个 | undefined?从源码设计看,这个 undefined 是为了让 TS 函数重载的匹配机制更稳健。在 infinite-query-options.ts:88-109 中,infiniteQueryOptions() 与 injectInfiniteQuery()(inject-infinite-query.ts:41-56)把 DefinedInitialDataInfiniteOptions 作为第一个也是最优先匹配的重载签名。由于字段本身是必填键(缺失该键的对象字面量无法命中本重载),凡是在编译期能确认提供了非空初始数据的调用,都会命中本类型,从而获得返回类型的收窄。联合末尾的 | undefined 则保证:当需要显式书写 initialData: undefined(例如把配置中心化保存后再统一传入)时不会直接造成类型报错,整体仍可落入 defined 分支的匹配范围。
三、泛型参数逐一解读
本类型一共声明五个泛型参数,与 core 层 InfiniteQueryObserverOptions 的泛型顺序保持一致:
| 参数 | 默认值 | 约束 | 含义 |
|---|---|---|---|
TQueryFnData |
—(必填推断) | — | 单页请求(queryFn)返回的原始数据类型 |
TError |
DefaultError |
— | 查询失败的错误类型,Angular Query 默认采用 core 的 DefaultError(即 Error 基类) |
TData |
InfiniteData<TQueryFnData> |
— | 最终暴露给消费方的数据类型;默认即原始数据包成 InfiniteData,若使用 select 转换可另行指定 |
TQueryKey |
QueryKey |
extends QueryKey |
查询键类型,约束为 core 层 QueryKey(即 readonly unknown[] 或类似可序列化元组) |
TPageParam |
unknown |
— | 翻页参数类型(如 number、string 或 cursor),同时用于约束 initialPageParam 与 getNextPageParam 的返回 |
在实际使用中,绝大多数场景无需手工显式写出这些泛型——TS 会从 queryFn、initialPageParam 与 getNextPageParam 自动推断 TQueryFnData 与 TPageParam,正如测试用例 infinite-query-options.test.ts:7-18 所示,仅传入普通对象即可完成类型推导。
四、类型分工:为什么需要 Defined / Undefined 两个变体?
回顾 infinite-query-options.ts 顶部,与 DefinedInitialDataInfiniteOptions 并列的还有:
UndefinedInitialDataInfiniteOptions(L13-L32):initialData?可选,其值同样受NonUndefinedGuard约束;UnusedSkipTokenInfiniteOptions(L34-L60):处理与SkipToken相关的条件化查询场景,与 initialData 无关。
这种拆分的动机写在 injectInfiniteQuery 的重载签名里(inject-infinite-query.ts:41-104):
// 重载一:命中 DefinedInitialDataInfiniteOptions → 返回 Defined 结果
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>
// 重载二:未提供 initialData → 返回常规结果(data 可能为 undefined)
export function injectInfiniteQuery<...>(
injectInfiniteQueryFn: () => UndefinedInitialDataInfiniteOptions<...>,
options?: InjectInfiniteQueryOptions,
): CreateInfiniteQueryResult<TData, TError>
两个重载的返回类型差异是整个设计的价值所在:
- 当
injectInfiniteQueryFn返回的对象含非空initialData(命中重载一),返回类型为DefinedCreateInfiniteQueryResult<TData, TError>,它由 types.ts:117-124 定义,等价于MapToSignals<DefinedInfiniteQueryObserverResult<...>>。翻译成开发者语言就是:data信号的类型不再是TData | undefined,而是确定的TData,status从一开始就是'success',读取时不需要额外的!或空值兜底。 - 当没有初始数据(命中重载二),返回
CreateInfiniteQueryResult,此时data可空,与"首次加载可能 pending"的真实运行时状态完全一致。
也就是说,"Defined" 前缀并非营销式命名,而是用 initialData 的必填约束换来了从创建那一刻起数据必然存在的类型承诺——这正是类型系统对"预填充缓存"这种运行时可证明事实的建模。
五、实战:在 Angular 组件中预填充无限查询
结合以上分析,下面给出一个可运行的最小示例。它复用已缓存的首页数据作为 initialData,让用户进入列表页时先看到缓存内容、后台再静默刷新:
import { Component, computed, inject } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { injectInfiniteQuery } from '@tanstack/angular-query-experimental'
import { InfiniteData } from '@tanstack/query-core'
interface Project {
id: number
name: string
}
interface Page {
items: Array<Project>
nextCursor: string | null
}
@Component({ ... })
export class ProjectListComponent {
private http = inject(HttpClient)
// 注意:injectInfiniteQueryFn 返回对象中提供了 initialData,
// 命中 DefinedInitialDataInfiniteOptions 重载。
readonly projects = injectInfiniteQuery(() => ({
queryKey: ['projects'] as const,
queryFn: ({ pageParam }) =>
this.http.get<Page>('/api/projects', {
params: { cursor: pageParam as string | undefined },
}),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
staleTime: 60_000,
initialData: {
pages: [{ items: [], nextCursor: null }],
pageParams: [undefined],
} satisfies InfiniteData<Page, string | undefined>,
}))
// 由于命中 Defined 分支,data() 的静态类型是确定的 InfiniteData,
// 无需再写 this.projects.data()?.pages 之类的空值守卫。
readonly list = computed(() => this.projects.data().pages.flatMap((p) => p.items))
}
几个落点提示:
initialData的结构必须同时给出pages与pageParams,且数组下标一一对应;此处用空页作为占位,适合"启动即空但想要 defined 结果"的场景。- 若初始数据来自二次存储(例如配合持久化中间件读取缓存),惰性函数形式更有价值:
initialData: () => readCachedInfiniteData(...)。它能让反序列化/读取操作延迟到真正创建查询时才执行,避免无谓开销。 injectInfiniteQuery返回的是 signal 化的结果对象(源码中MapToSignals将每个状态字段映射为 signal),模板或computed中需以调用形式data()、fetchNextPage()使用。
六、边界与最佳实践
- 不要用它去建模"可能没有数据"的查询。如果首屏数据需要异步拉取、开始时可能为空,应让 TS 走
UndefinedInitialDataInfiniteOptions分支,返回可空data,并配合模板@if或isPending信号做分支渲染。 - 注意 freshness 判定基准。core 层 types.ts:259-260 将
initialData与initialDataUpdatedAt并列定义。默认情况下staleTime以初始数据写入时刻起算,若初始数据来自陈旧缓存,可配合initialDataUpdatedAt: Date.now() - 间隔让 TanStack 及时判定其为 stale 并触发后台重取。本 Angular 类型完整继承该字段(源自CreateInfiniteQueryOptions的展开),可直接传入。 initialData与placeholderData语义不同。前者会被纳入查询数据(影响dataUpdatedAt、参与缓存合并),用于"确实已有数据"的预填充;若只是想占位展示 UI 而数据仍从网络获取,应使用placeholderData而不是本类型。- 集中管理选项时使用
infiniteQueryOptions()。若在 Service 层以as const或工厂函数集中编写选项,可显式标注返回值为infiniteQueryOptions({ ... })。该函数在 infinite-query-options.ts:88-109 定义了接受DefinedInitialDataInfiniteOptions的重载,返回时会附带QueryKeyWithDataTag——用queryFn的推断类型回注queryKey,使跨模块共享选项时queryKey也能获得类型标签。其运行时实现非常简单(L178-L180),只是原样返回入参,纯编译期工具。
七、结语
DefinedInitialDataInfiniteOptions 是 Angular Query 类型体操的典型样例:它没有新增任何运行时逻辑,纯粹通过"让 initialData 成为必填键 + NonUndefinedGuard 排除 undefined"这两个类型层面的手段,把 injectInfiniteQuery 的返回结果从"可能为空"收窄为"必然已定义"。理解它,等于同时理解了 TanStack Query 在 Angular 侧 Defined/Undefined 双分支重载的整体设计,也就能在真实项目中准确选择:有可复用的初始多页数据,就用 DefinedInitialDataInfiniteOptions 换取无守卫的 data 类型;没有,就放心交给 UndefinedInitialDataInfiniteOptions 处理加载态。
参考源码索引
- 类型别名定义与兄弟类型:infinite-query-options.ts
injectInfiniteQuery重载与实现:inject-infinite-query.ts- Angular 侧选项/结果类型基座:types.ts
- core 层基础设施(
InfiniteData、NonUndefinedGuard、initialData):query-core/src/types.ts - 工具函数行为测试:infinite-query-options.test.ts
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 StartedRust0626
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00