首页
/ Angular Query 的 CreateInfiniteQueryResult 类型全解析:从注入无限查询到 Signal 化结果对象

Angular Query 的 CreateInfiniteQueryResult 类型全解析:从注入无限查询到 Signal 化结果对象

2026-09-07 12:38:00作者:鲍丁臣Ursa

本篇文章围绕 Angular Query(@tanstack/angular-query-experimental)核心公开类型之一——CreateInfiniteQueryResult 展开。它是由 injectInfiniteQuery() 返回的结果类型,用于承载无限滚动 / 分页加载查询的完整状态。阅读本文后,你将理解该类型“三段式组合”的构成原理(状态窄化守卫 + Signal 映射 + query-core 观察者结果),掌握 hasNextPagefetchNextPagedata.pages 等成员的精确语义,并能在真实组件中正确使用 CreateInfiniteQueryResult / DefinedCreateInfiniteQueryResult 编写类型安全的无限查询。

CreateInfiniteQueryResult 是什么

type-aliases/CreateInfiniteQueryResult.md 中,该类型的完整定义只有一行,却浓缩了 Angular Query 结果体系的全部设计:

type CreateInfiniteQueryResult<TData, TError> =
  BaseQueryNarrowing<TData, TError> &
  MapToSignals<InfiniteQueryObserverResult<TData, TError>>

它由三部分通过交叉类型(&)组合而成:

组成 来源 职责
BaseQueryNarrowing<TData, TError> types.ts 提供 isSuccess / isError / isPending 三个状态窄化类型守卫
MapToSignals<...> signal-proxy.ts 把对象中的每个字段递归包装为 Angular Signal,函数字段原样透传
InfiniteQueryObserverResult<TData, TError> query-core/src/types.ts 无限查询观察者底层结果(含分页字段与状态判别联合)

换句话说:CreateInfiniteQueryResult 就是 query-core 中 InfiniteQueryObserverResult 的 Angular 化形态——原本的普通属性值变成可响应式读取的 Signal,而方法与状态守卫保持函数语义不变。

类型参数 TDataTError

定义中声明的两个类型参数及其默认值:

  • TData(默认 unknown):查询成功时 data 的数据类型。对于无限查询,泛型上界并非单页数据,而是整个分页聚合结构 InfiniteData<TQueryFnData, TPageParam>(见下文“pages 结构”)。
  • TError(默认 DefaultError):查询失败时 error 的类型。DefaultError 定义于 @tanstack/query-core,是允许通过 Register 接口做模块增强替换的默认错误类型,不显式声明时即按默认错误语义处理。

从注入函数看返回类型的来源

CreateInfiniteQueryResult 不是凭空定义的抽象类型,而是 injectInfiniteQuery 的实际返回值。在 inject-infinite-query.ts 的实现中:

export function injectInfiniteQuery(
  injectInfiniteQueryFn: () => CreateInfiniteQueryOptions,
  options?: InjectInfiniteQueryOptions,
) {
  !options?.injector && assertInInjectionContext(injectInfiniteQuery)
  const injector = options?.injector ?? inject(Injector)
  return runInInjectionContext(injector, () =>
    createBaseQuery(
      injectInfiniteQueryFn,
      InfiniteQueryObserver as typeof QueryObserver,
    ),
  )
}

可以看到:它把选项函数与 InfiniteQueryObserver 一起交给通用的 createBaseQuery 创建观察者实例,因此结果中的 datastatusfetchStatus 等字段天然带有无限查询(分页)特有语义,而非普通 injectQuery 的返回结果。

三个重载:按 initialData 情况区分返回类型

injectInfiniteQuery 针对“选项函数返回的 options 是否保证携带 initialData”提供了三种重载签名:

这一设计让 TS 编译器能够根据选项形态自动推断结果:一旦提供 initialDatadata 字段即为“有值”类型;未提供时则保留 data: undefined 的可能,需由开发者在模板或逻辑中处理加载态。

底层 InfiniteQueryObserverResult:与普通查询结果的分页差异

MapToSignals 所包裹的对象本体是 query-core 的 InfiniteQueryObserverResult。它由多个按 status 区分的联合成员构成:

export type InfiniteQueryObserverResult<TData, TError> =
  | DefinedInfiniteQueryObserverResult<TData, TError>   // refetchError | success
  | InfiniteQueryObserverLoadingErrorResult<TData, TError>
  | InfiniteQueryObserverLoadingResult<TData, TError>
  | InfiniteQueryObserverPendingResult<TData, TError>
  | InfiniteQueryObserverPlaceholderResult<TData, TError>

每个联合分支都扩展自 InfiniteQueryObserverBaseResult,在普通查询结果基础上额外追加了六个无限查询专属成员:

成员 类型 语义
fetchNextPage(options?) (...) => Promise<InfiniteQueryObserverResult> 触发加载“下一页”
fetchPreviousPage(options?) (...) => Promise<InfiniteQueryObserverResult> 触发加载“上一页”
hasNextPage boolean 依据 getNextPageParam 判断是否还有下一页
hasPreviousPage boolean 依据 getPreviousPageParam 判断是否还有上一页
isFetchNextPageError boolean 加载下一页期间是否出错
isFetchingNextPage boolean 是否正在加载下一页(用于禁用“加载更多”按钮)
isFetchPreviousPageError boolean 加载上一页期间是否出错
isFetchingPreviousPage boolean 是否正在加载上一页

正是由于底层 InfiniteQueryObserverResult状态判别联合,不同状态下字段的取值会收窄:例如 pending 分支 data: undefinederror: nullisSuccess: falsesuccess 分支则 data: TDataisSuccess: true(详见 query-core/src/types.ts)。

data 的结构:InfiniteData

无限查询的 data 不是单页数据,而是聚合容器 InfiniteData

export interface InfiniteData<TData, TPageParam = unknown> {
  pages: Array<TData>          // 已加载的各页数据,按顺序排列
  pageParams: Array<TPageParam> // 每页对应的 pageParam
}

因此在模板中遍历数据时,往往需要两层循环:先遍历 data().pages,再遍历每个 page 内部的数据项(见下文完整示例)。

BaseQueryNarrowing:三个状态窄化守卫

结果对象上的状态判断并非普通 boolean,而是 TS 类型谓词(type predicate)。BaseQueryNarrowing 定义在 types.ts

export interface BaseQueryNarrowing<TData = unknown, TError = DefaultError> {
  isSuccess: (
    this: CreateBaseQueryResult<TData, TError>,
  ) => this is CreateBaseQueryResult<TData, TError, CreateStatusBasedQueryResult<'success', TData, TError>>
  isError: (
    this: CreateBaseQueryResult<TData, TError>,
  ) => this is CreateBaseQueryResult<TData, TError, CreateStatusBasedQueryResult<'error', TData, TError>>
  isPending: (
    this: CreateBaseQueryResult<TData, TError>,
  ) => this is CreateBaseQueryResult<TData, TError, CreateStatusBasedQueryResult<'pending', TData, TError>>
}

三个方法都声明了 this is ... 谓词,配合内部工具类型 CreateStatusBasedQueryResult(通过 Extract 从结果联合中取出对应 status 的分支),实现条件收窄:当 isSuccess() 返回 true 时,编译器自动知道当前对象的 data 非空、status: 'success'。这是 Angular 模板中 @if (query.isSuccess()) { ... } 分支内能安全解引用 query.data() 的类型学依据。

在 Angular Query 的上下文中,这些守卫函数是方法调用形式query.isSuccess()),与 React Query 中属性式 isSuccess 的布尔判断存在明显差别——调用后返回类型谓词布尔值,与模板语法天然契合。

MapToSignals:字段到 Signal 的映射机制

CreateInfiniteQueryResult 与核心结果类型最大的差异在 MapToSignals。其定义位于 signal-proxy.ts

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

映射规则很直接:

  • 函数类型字段(extends Function)原样保留——例如 fetchNextPagefetchPreviousPagerefetchisSuccess 等仍是普通方法,可直接以 query.fetchNextPage() 调用;
  • 其余字段包装为 Signal<T[K]>——例如 data 变成 Signal<InfiniteData<...>>status 变成 Signal<'pending' | 'success' | 'error'>hasNextPage 变成 Signal<boolean>

运行时实现:signalProxy

类型层面由 MapToSignals 完成,运行时则由 signalProxyProxy 实现:

export function signalProxy<TInput extends Record<string | symbol, any>>(
  inputSignal: Signal<TInput>,
) {
  const internalState = {} as MapToSignals<TInput>
  return new Proxy<MapToSignals<TInput>>(internalState, {
    get(target, prop) {
      const computedField = target[prop]
      if (computedField) return computedField
      const targetField = untracked(inputSignal)[prop]
      if (typeof targetField === 'function') return targetField
      return (target[prop] = computed(() => inputSignal()[prop]))
    },
    // has / ownKeys / getOwnPropertyDescriptor ...
  })
}

该实现的关键行为是惰性 + 缓存:首次访问某字段时,通过 computed(() => inputSignal()[prop]) 生成一个 Computed signal 并缓存进 internalState;之后读取直接命中缓存。函数字段则从底层结果对象中取原引用透传返回,保证调用时 this 与真实方法一致。untracked 的运用使“方法是否函数”的判断不建立响应式依赖,避免不必要的重算。

模板中的读取方式

映射完成后,模板里的访问必须区分两种语法:

  • query.data().pages —— data 是 Signal,用 () 调用读取
  • query.fetchNextPage() —— fetchNextPage 是方法,直接调用即可。

若误写 query.data.pages(漏掉括号)则拿不到数据,这是从 Signal 化结果对象迁移时最常见的错误。

完整实战:基于 CreateInfiniteQueryResult 的分页组件

结合 guides/infinite-queries.md 中的官方示例,一个完整的分页加载组件如下。注意类型推断:未提供 initialData 时,query 的静态类型即为 CreateInfiniteQueryResult<InfiniteData<Project>, Error>

import { Component, computed, inject } from '@angular/core'
import { injectInfiniteQuery } 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)

  query = injectInfiniteQuery(() => ({
    queryKey: ['projects'],
    queryFn: async ({ pageParam }) => {
      return lastValueFrom(this.projectsService.getProjects(pageParam))
    },
    initialPageParam: 0,
    getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined,
    getNextPageParam: (lastPage) => lastPage.nextId ?? undefined,
    maxPages: 3,
  }))

  // hasNextPage / isFetchingNextPage 是 Signal,需用 () 读取
  nextButtonDisabled = computed(
    () => !this.query.hasNextPage() || this.query.isFetchingNextPage(),
  )
  nextButtonText = computed(() =>
    this.query.isFetchingNextPage()
      ? 'Loading more...'
      : this.query.hasNextPage()
        ? 'Load newer'
        : 'Nothing more to load',
  )
}

模板利用 isPending() / isError() / isSuccess() 的窄化能力,配合两层 @for 遍历 data().pages

<div>
  @if (query.isPending()) {
    <p>Loading...</p>
  } @else if (query.isError()) {
    <span>Error: {{ query.error()?.message }}</span>
  } @else {
    @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]="nextButtonDisabled()">
        {{ nextButtonText() }}
      </button>
    </div>
  }
</div>

组件中 nextButtonDisabledcomputed() 把多个 Signal(hasNextPageisFetchingNextPage)组合成派生 Signal,避免直接在模板中书写复杂条件,这正是 Signal 化结果对象在 Angular 响应式体系下的推荐用法。

滚动触底自动加载场景

若是“无限滚动”而非按钮点击,可在触底事件回调中先做防抖判断再取页。文档 infinite-queries.md 也给出了基于 isFetching() 守卫的范式:

@Component({
  template: ` <list-component (endReached)="fetchNextPage()" /> `,
})
export class Example {
  query = injectInfiniteQuery(() => ({
    queryKey: ['projects'],
    queryFn: async ({ pageParam }) => {
      return lastValueFrom(this.projectsService.getProjects(pageParam))
    },
  }))

  fetchNextPage() {
    // Do nothing if already fetching
    if (this.query.isFetching()) return
    this.query.fetchNextPage()
  }
}

DefinedCreateInfiniteQueryResult 的关系与取舍

当选项显式携带 initialData(或 placeholderData 的相关形态)时,injectInfiniteQuery 返回的是兄弟类型 DefinedCreateInfiniteQueryResult

type DefinedCreateInfiniteQueryResult<
  TData, TError,
  TDefinedInfiniteQueryObserver = DefinedInfiniteQueryObserverResult<TData, TError>,
> = MapToSignals<TDefinedInfiniteQueryObserver>

两者关键差异有两点:

  1. 底层联合范围不同DefinedCreateInfiniteQueryResult 包裹的是 DefinedInfiniteQueryObserverResult,仅含 successrefetchError 两个分支(外加 placeholder 语义),不含 pending / loading 分支。因此它不再需要 isPending / isSuccess 这类 BaseQueryNarrowing 守卫——data 恒有值。
  2. 构成不同CreateInfiniteQueryResult = 窄化守卫 & Signal 化结果;DefinedCreateInfiniteQueryResult = 纯 Signal 化结果。

从实现看,create-base-query.ts 内部先通过 observerSignal().getOptimisticResult(...) 计算“乐观结果”,再由订阅回调更新状态。是否“已定义(defined)”由选项是否保证 initialData 决定,最终体现为返回类型在 CreateInfiniteQueryResultDefinedCreateInfiniteQueryResult 间的切换。这提示开发实践:在查询初始化阶段能够提供初始页数据(如 SSR 注入或缓存回填)时,尽量使用 initialData 形态,可获得免去 loading 分支的更窄类型;否则使用 CreateInfiniteQueryResult 并在模板/代码中显式处理 isPending

结果对象在 Angular 响应式系统中的定位小结

综合 types.tssignal-proxy.tsquery-core/src/types.ts 的实现,可以归纳 CreateInfiniteQueryResult 的三层价值:

  • 状态完整:完整继承 query-core InfiniteQueryObserverResult 的状态判别联合,pending / error / success 及分页状态一应俱全;
  • 形态响应式:经 MapToSignals 将值字段转成 Signal,与 Angular 组件模板、computed 派生状态无缝协作;
  • 类型安全BaseQueryNarrowing 的谓词守卫让条件分支内的 dataerror 自动收窄,配合 InfiniteDatapages / pageParams 结构,让“无限滚动 / 加载更多”这类高频需求获得端到端的编译期保障。

在实际项目中,理解该类型即可顺手推及 CreateQueryResult(普通查询)等兄弟类型:它们共用 MapToSignalsBaseQueryNarrowing,仅在底层观察者结果上有所差异。建议在查询逻辑层为“返回的结果对象”显式声明 CreateInfiniteQueryResult<InfiniteData<T>> 之类别名,既能提升可读性,也有助于团队统一分页查询的契约。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 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
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.79 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
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
390