首页
/ Angular Query 类型详解:DefinedCreateInfiniteQueryResult 与「数据必有值」的 Signal 化无限查询结果

Angular Query 类型详解:DefinedCreateInfiniteQueryResult 与「数据必有值」的 Signal 化无限查询结果

2026-09-07 19:35:46作者:幸俭卉

DefinedCreateInfiniteQueryResult 是 Angular Query(@tanstack/angular-query-experimental)在**确保查询结果数据一定存在(defined)**时返回的无限查询(Infinite Query)结果类型。它以 @tanstack/query-coreDefinedInfiniteQueryObserverResult 为数据源,再经 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
  • 语义:InfiniteQueryObserverResultDefinedInfiniteQueryObserverResult 中的 data 都是 InfiniteData<TData, TPageParam> 形式(含 pagespageParams 两个数组)。当从 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>

两者中 InfiniteQueryObserverSuccessResultInfiniteQueryObserverRefetchErrorResult(首次已成功、后续刷新失败仍保留旧数据)都声明了非空字段(data: TDataisError: false 等,见 types.ts);被排除在外的 LoadingErrorResultLoadingResult 恰恰是“查询还未拿到任何数据”的状态。

因此,从源码结构可以推断:只要结果类型是 Defined 系,就等价于保证无限查询已经从缓存或网络中拿到了一页以上数据,模板中不再需要为“data 不存在”的初载场景兜底。

MapToSignals:把 Plain Object 结果改造成 Signal 集合

DefinedCreateInfiniteQueryResult 之所以能被 Angular 模板响应式读取,核心转换器是 MapToSignalssignal-proxy.ts):

export type MapToSignals<T> = {
  [K in keyof T]: T[K] extends Function ? T[K] : Signal<T[K]>
}

两个要点需要展开:

  1. 属性 → 信号:所有值为普通数据的字段被映射为 Signal<T[K]>,因此组件里访问 query.data()query.hasNextPage()query.isPending()query.fetchStatus() 等都是调用信号读取最新值,Angular 变更检测会自动订阅;
  2. 方法 → 原样透传:凡属性值是函数(例如 fetchNextPagefetchPreviousPagerefetchremove),映射后仍保持函数类型,模板中可直接以 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
  • hasNextPageisFetchingNextPage 字段经 MapToSignals 变成信号,类字段 #hasNextPage 实际存的是信号,模板中调用需写作 #hasNextPage()
  • fetchNextPage 等函数被透传为普通方法,因此 (click)="query.fetchNextPage()" 直接可执行。

若去掉 initialData,同样的组件返回普通 CreateInfiniteQueryResult,此时 query.data() 可能为 undefined,官方示例中就需要用 @if (query.isPending()) 先做分支渲染(infinite-queries.md)。

与相邻结果类型的横向对比

为了让读者在重载签名和类型收窄之间不迷路,下表按 types.ts 归纳:

类型 底层状态 结构差异 数据是否必有
CreateBaseQueryResult QueryObserverResult 交叉 BaseQueryNarrowingisSuccess 等守卫,见 types.ts
CreateQueryResult QueryObserverResult 同上(查询专用别名)
DefinedCreateQueryResult DefinedQueryObserverResult 同上但状态为 Defined 联合 是(普通查询)
CreateInfiniteQueryResult InfiniteQueryObserverResult 交叉 BaseQueryNarrowing + MapToSignals 否(首次加载可能无 data)
DefinedCreateInfiniteQueryResult DefinedInfiniteQueryObserverResult MapToSignals,无收窄守卫 是(无限查询)

一个容易忽略的点:CreateInfiniteQueryResult 交叉了 BaseQueryNarrowing,用 query.isSuccess() 等方式做类型守卫;而 DefinedCreateInfiniteQueryResult 不再需要守卫——因为联合类型里根本不存在“无 data”的分支,MapToSignals 直接整体转换即得到安全结果。

使用建议与边界说明

  • 优先让类型自动推导:不要手写三个类型参数,让 injectInfiniteQuery(() => ({ ..., initialData })) 的重载推导完成收窄;infiniteQueryOptionsinfinite-query-options.ts)可把 Defined 选项在模块间以类型安全的方式共享复用。
  • 理解“Defined”≠“永远在请求”:Defined 只承诺 data 有值(含刷新失败后保留的陈旧数据),不承诺查询空闲或正在后台刷新——isFetchingfetchStatus 仍需按业务需要读取,这与 DefinedInfiniteQueryObserverResultRefetchError 分支的含义一致。
  • 模板与类型脱钩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”双轨设计的取舍逻辑。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389