首页
/ 深入解析 Angular Query 的 `DefinedInitialDataInfiniteOptions`:用类型系统驾驭带初始数据的无限查询

深入解析 Angular Query 的 `DefinedInitialDataInfiniteOptions`:用类型系统驾驭带初始数据的无限查询

2026-09-07 09:34:45作者:董宙帆

导读

在 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
}

可以拆解为两层含义:

  1. 继承全部无限查询选项CreateInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> 是 Angular Query 对 core 层 InfiniteQueryObserverOptions 的轻量封装。查看 types.ts:75-90 可知,它仅通过 OmitKeyof 剔除掉了 suspense 字段,其余(queryKeyqueryFngetNextPageParaminitialPageParammaxPagesgcTimestaleTime 等)全部沿用 core 语义。

  2. 在继承之上"收紧"了 initialData:交叉(&)出一个只含单一成员 initialData 的对象,且该成员被声明为必填键(注意:initialData 后没有 ?)。这正是与兄弟类型 UndefinedInitialDataInfiniteOptionsinfinite-query-options.ts:13-32,其中 initialData? 为可选)的关键差异。

值得说明:Angular Query 中该类型的定义方式与文档页展示略有差异——类型参数列表里 TErrorTDataTQueryKeyTPageParam 在源码中都有默认值与约束,文档页属于生成式的 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 | undefinedtypes.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 翻页参数类型(如 numberstring 或 cursor),同时用于约束 initialPageParamgetNextPageParam 的返回

在实际使用中,绝大多数场景无需手工显式写出这些泛型——TS 会从 queryFninitialPageParamgetNextPageParam 自动推断 TQueryFnDataTPageParam,正如测试用例 infinite-query-options.test.ts:7-18 所示,仅传入普通对象即可完成类型推导。

四、类型分工:为什么需要 Defined / Undefined 两个变体?

回顾 infinite-query-options.ts 顶部,与 DefinedInitialDataInfiniteOptions 并列的还有:

  • UndefinedInitialDataInfiniteOptionsL13-L32):initialData? 可选,其值同样受 NonUndefinedGuard 约束;
  • UnusedSkipTokenInfiniteOptionsL34-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,而是确定的 TDatastatus 从一开始就是 '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 的结构必须同时给出 pagespageParams,且数组下标一一对应;此处用空页作为占位,适合"启动即空但想要 defined 结果"的场景。
  • 若初始数据来自二次存储(例如配合持久化中间件读取缓存),惰性函数形式更有价值:initialData: () => readCachedInfiniteData(...)。它能让反序列化/读取操作延迟到真正创建查询时才执行,避免无谓开销。
  • injectInfiniteQuery 返回的是 signal 化的结果对象(源码中 MapToSignals 将每个状态字段映射为 signal),模板或 computed 中需以调用形式 data()fetchNextPage() 使用。

六、边界与最佳实践

  1. 不要用它去建模"可能没有数据"的查询。如果首屏数据需要异步拉取、开始时可能为空,应让 TS 走 UndefinedInitialDataInfiniteOptions 分支,返回可空 data,并配合模板 @ifisPending 信号做分支渲染。
  2. 注意 freshness 判定基准。core 层 types.ts:259-260initialDatainitialDataUpdatedAt 并列定义。默认情况下 staleTime 以初始数据写入时刻起算,若初始数据来自陈旧缓存,可配合 initialDataUpdatedAt: Date.now() - 间隔 让 TanStack 及时判定其为 stale 并触发后台重取。本 Angular 类型完整继承该字段(源自 CreateInfiniteQueryOptions 的展开),可直接传入。
  3. initialDataplaceholderData 语义不同。前者会被纳入查询数据(影响 dataUpdatedAt、参与缓存合并),用于"确实已有数据"的预填充;若只是想占位展示 UI 而数据仍从网络获取,应使用 placeholderData 而不是本类型。
  4. 集中管理选项时使用 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 处理加载态。

参考源码索引

登录后查看全文
热门项目推荐
相关项目推荐