首页
/ @tanstack/lit-query 的 infiniteQueryOptions:让 queryKey 承载无限查询数据类型,实现跨 API 的类型安全

@tanstack/lit-query 的 infiniteQueryOptions:让 queryKey 承载无限查询数据类型,实现跨 API 的类型安全

2026-09-07 09:26:40作者:侯霆垣

本文讲解 @tanstack/lit-query 提供的类型辅助函数 infiniteQueryOptions:它在编译期将「分页数据形状(InfiniteData)」与「错误类型」以品牌标记(DataTag)的形式绑定到 queryKey 上,使同一份 options 在 createInfiniteQueryControllerqueryClient.getQueryDataqueryClient.setQueryDataqueryClient.infiniteQuery 等 TanStack Query API 之间传递时保持精确的类型推断。读完本文,你将理解该函数的签名、五个泛型参数的职责、运行时零成本的实现原理,以及如何在 Lit 组件中把它与无限查询控制器组合出带类型保护的「加载更多」分页方案。

函数签名与核心作用

infiniteQueryOptions 的完整签名如下(该文档定义见 packages/lit-query/src/infiniteQueryOptions.ts,源码中函数文档注释亦与其一致):

function infiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>(options): InfiniteQueryObserverOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & object;

其官方说明是:

Brands infinite query options so the queryKey carries the infinite query data and error types across TanStack Query APIs.

即:对无限查询 options 做"品牌化"(branding),使 queryKey 能够把无限查询的数据类型与错误类型携带到 TanStack Query 的各条 API 中

之所以需要这样一个辅助函数,是因为无限查询的缓存值结构特殊——它不是单页数据,而是 { pages: TPage[]; pageParams: TPageParam[] } 的聚合结构(下文详述)。若直接手写 queryKey: ['projects'] 并交给 queryClient.setQueryDataqueryClient.prefetchInfiniteQuery 等方法,TypeScript 无从得知该 key 对应的数据到底是什么;而经过 infiniteQueryOptions 包装后,查询键的类型信息(数据与错误类型)会随对象一起传播,调用方无需重复声明泛型即可拿到精确的读写类型。

五个类型参数逐一解析

infiniteQueryOptions 是一个完全类型层面的函数,其泛型顺序与 InfiniteQueryObserverOptions 保持一致,每个参数的含义与默认值如下:

类型参数 约束 / 默认值 含义
TQueryFnData 默认 unknown queryFn 每次调用返回的单页数据类型。这是最核心的一层,后面的 InfiniteData 由它推导而来
TError 默认 Error 请求失败时 error 字段的类型。文档默认值为 Error;在 query-core 的源码 中该默认通过 DefaultError 表达——它由全局 Register 接口解析,未增补时解析结果即 Error,因此在大多数项目中二者等价
TData 默认 InfiniteData<TQueryFnData> 查询结果 data 的整体形状。无限查询默认会把这 N 页数据聚合成 InfiniteData,即 { pages: Array<TData>; pageParams: Array<TPageParam> }(见 types.ts 中 InfiniteData 的定义)。只有显式传入 select 转换结果时才需要改这一层
TQueryKey extends readonly unknown[],默认 readonly unknown[] queryKey 的类型。传入 ['projects'] as const 之类的常量元组可让 key 的字面量精确化,便于与服务端/缓存命中保持一致
TPageParam 默认 unknown 分页参数类型,即传给 queryFn 上下文 pageParam 字段的类型,同时也决定了 pageParams 数组的元素类型。当 queryKey 携带品牌信息后,InfiniteData<TQueryFnData, TPageParam> 会把该类型一并刻进 queryKey

参数、返回值与官方示例

参数 options

options: InfiniteQueryObserverOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>

这是待"保存并品牌化"的无限查询配置对象,其形状在 query-core 中被定义为 InfiniteQueryObserverOptions,包含 queryKeyqueryFninitialPageParamgetNextPageParamgetPreviousPageParammaxPages 等无限查询专属字段。其中 queryFn 收到的上下文为 QueryFunctionContextpageParam 字段的类型正是泛型 TPageParam

返回值

函数返回同一个 options 对象,其类型被收窄为:

InfiniteQueryObserverOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & {
  queryKey: DataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>
}

也就是说:原有配置项一个不少,额外多出的唯一变化是 queryKey 被升级为携带数据/错误类型的品牌键。

文档给出的示例

import { infiniteQueryOptions } from '@tanstack/lit-query'

const projectsOptions = infiniteQueryOptions({
  queryKey: ['projects'],
  queryFn: ({ pageParam }) => fetchProjects(pageParam),
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

运行时零成本:实现只有"原样返回"

很多人会好奇品牌化是否带来运行开销。查看 infiniteQueryOptions.ts 的实现 可以发现,除去类型签名后的函数体只有一行:

export function infiniteQueryOptions(options: unknown) {
  return options
}

不克隆、不改写、不冻结传入的对象,也不在 queryKey 上注入任何真实属性——所有信息都只存在于 TypeScript 类型层面。这意味着:

  • 它是纯编译期工具,与 queryOptions(见 queryOptions.ts)、mutationOptions(见 mutationOptions.ts)属于同一族辅助函数,三者都在 lit-query 的入口文件 中一并导出;
  • 由于返回的是原对象引用,把它传入依赖引用相等性做优化的场景(如 staleTime 判定、effect 去重)也完全安全;
  • 唯一的"成本"发生在编辑期——换来的是所有下游 API 的精确类型。

底层原理:DataTag 如何让 queryKey 携带类型

品牌化机制由 query-core 的类型体系支撑。在 query-core 的 types.ts 中:

export const dataTagSymbol = Symbol()
export const dataTagErrorSymbol = Symbol()
export type dataTagErrorSymbol = typeof dataTagErrorSymbol

export type DataTag<TType, TValue, TError = UnsetMarker> =
  TType extends AnyDataTag ? TType
  : TType & {
      [dataTagSymbol]: TValue
      [dataTagErrorSymbol]: TError
    }

DataTag 借助两个唯一的 Symbol 键作为"品牌的纹章",把两类元数据织入类型:

  1. [dataTagSymbol]:保存数据类型,对无限查询而言即 InfiniteData<TQueryFnData>
  2. [dataTagErrorSymbol]:保存错误类型 TError

查询键因此从"只是一个数组"变成"知道自己是哪份无限查询缓存的键"。随后 query-core 通过两个推断工具类型还原这些信息:

凡是接收查询键的 API——如 getQueryDatasetQueryDatafetchQueryprefetchInfiniteQuery 等——都会走这条还原链路,于是 queryKey 携带的类型信息得以跨 API 传播。这也解释了为什么 createInfiniteQueryController 的 options 参数类型 CreateInfiniteQueryOptions(见 createInfiniteQueryController.ts)会原样继承 InfiniteQueryObserverOptions:控制器内部通过 client.defaultQueryOptions(...) 处理传入的 options(同一文件),被品牌化后的 options 无论传给控制器、Observer 还是缓存方法,类型都不会"掉线"。

实战:与 createInfiniteQueryController 组合成类型安全的分页

无限查询控制器的用法记录在 Lit 框架的 infinite-queries 指南。把 infiniteQueryOptions 定义好的配置直接交给 createInfiniteQueryController,即可在控制器、缓存读写与指令式翻页之间共享同一份类型:

import { LitElement, html } from 'lit'
import {
  infiniteQueryOptions,
  createInfiniteQueryController,
} from '@tanstack/lit-query'

type ProjectPage = {
  projects: Array<{ id: number; name: string }>
  nextCursor?: number
}

const projectsOptions = infiniteQueryOptions({
  queryKey: ['projects'] as const,
  queryFn: ({ pageParam }) => fetchProjectsPage(pageParam),
  initialPageParam: 1,
  getNextPageParam: (lastPage) =>
    lastPage.nextCursor ?? undefined,
})

class ProjectsList extends LitElement {
  private readonly projects = createInfiniteQueryController(this, projectsOptions)

  render() {
    const query = this.projects()

    if (query.isPending) return html`Loading...`
    if (query.isError) return html`Error: ${query.error.message}`

    return html`
      ${query.data.pages.map(
        (page) => html`
          ${page.projects.map((project) => html`<p>${project.name}</p>`)}
        `,
      )}
      <button
        ?disabled=${!query.hasNextPage || query.isFetching}
        @click=${() => this.projects.fetchNextPage()}
      >
        ${query.isFetchingNextPage ? 'Loading more...' : 'Load More'}
      </button>
    `
  }
}

这里的结果对象 query.data 自动被推断为 InfiniteData<ProjectPage> | undefined,因此 query.data.pages 中每个元素的类型都是精确的 ProjectPage

控制器返回的 accessor 还额外暴露了 refetchfetchNextPagefetchPreviousPagedestroy 方法,它们都委托给底层 InfiniteQueryObserver(见 createInfiniteQueryController.ts)。若组件未位于 QueryClientProviderQueryClientProvider.ts)上下文内,这些方法会以 No QueryClient available 错误确定性地失败,控制器进入占位 pending 状态——测试用例 LC-INF-01 / LC-INF-03 对此有专门覆盖。

当配置依赖组件响应式状态时

Lit 的控制器体系支持把 options 作为函数传入:当 options 是函数时,控制器会在宿主更新期间重新读取(见 createInfiniteQueryController.ts),因此 queryKey 可以跟随宿主响应式状态变化。此时把 infiniteQueryOptions 放进 getter 即可:

private readonly projects = createInfiniteQueryController(
  this,
  () => infiniteQueryOptions({
    queryKey: ['projects', this.categoryId] as const,
    queryFn: ({ pageParam }) => fetchProjectsPage(this.categoryId, pageParam),
    initialPageParam: 1,
    getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
  }),
)

跨 API 复用:缓存读写也能拿到精确类型

infiniteQueryOptions 最实用的价值,是把"选项定义一次,全项目类型一致"落实到位。仓库的类型测试 type-inference.test.ts 验证了下列链路:

const infiniteQueryOpts = infiniteQueryOptions({
  queryKey: ['type-inference', 'infinite-query-options'] as const,
  initialPageParam: 0,
  queryFn: async () => ({ page: 3 }),
  getNextPageParam: (lastPage) => lastPage.page + 1,
})

// 品牌刻在 queryKey 上:
// dataTagSymbol -> InfiniteData<{ page: number }>
// dataTagErrorSymbol -> Error
const cachedPages = client.getQueryData(infiniteQueryOpts.queryKey)
// cachedPages: InfiniteData<{ page: number }> | undefined

const updatedPages = client.setQueryData(infiniteQueryOpts.queryKey, {
  pages: [{ page: 4 }],
  pageParams: [0],
})
// updatedPages: InfiniteData<{ page: number }> | undefined

关键收益:

  • 读缓存getQueryData 不再返回 unknown,而是 InfiniteData<TPage> | undefined
  • 写缓存setQueryData 的入参与回调参数(如乐观更新中基于上一份数据合并新页)都被限定为正确的 InfiniteData 结构,传入 { pages: [...] } 之外的错误形状会直接编译报错;
  • 命令式无限查询client.infiniteQuery(options) 也能借助品牌键还原出完整类型,测试 L7–L9(type-inference.test.ts)进一步验证了 select 转换(返回 data.pages 数组)与 enabled: false 组合时同样成立。

同时,集成测试 OPT-01(infinite-and-options.test.ts)从运行时角度证实:把 infiniteQueryOptions 包装的配置交给 createInfiniteQueryController 后,数据可正常请求并聚合为 data.pages,证明该函数在"类型增强"之外对运行时行为毫无侵入。

与 queryOptions、mutationOptions 的分工

infiniteQueryOptions 并非孤立设计,它和同一包的 queryOptions(普通查询)与 mutationOptions(变更操作)构成一组配套的类型入口:

辅助函数 绑定到 queryKey 的数据类型 面向场景
queryOptions 普通 TQueryFnData(单页直出) 单次请求、按 key 缓存
infiniteQueryOptions InfiniteData<TQueryFnData, TPageParam>(pages + pageParams 聚合) 无限滚动、"加载更多"列表
mutationOptions 无(变更不共享查询键) 写操作,配合失效与乐观更新

三者的实现策略一致:均为重载 + options 原样返回的零运行时工具(packages/lit-query/src 下同名文件)。其中 queryOptions 还针对 initialData 是否存在拆分出 DefinedInitialDataOptions / UndefinedInitialDataOptions 等重载,用于在 data 非空的场景下收紧类型;infiniteQueryOptions 则保持单一重载,把所有类型信息统一通过 DataTag 收敛到 queryKey 上。

使用注意事项

  • 类型仅存在于编译期DataTag 是纯类型层面的品牌(虽然 query-core 导出了 dataTagSymbol 这一常量,但它只作为类型键使用),运行时 queryKey 仍是普通数组,不要试图在代码里读取 queryKey[dataTagSymbol] 获取真实值;
  • 让 queryKey 足够精确:要获得元组级推断,建议书写字面量 key(如 ['projects', id] as const)或让类型参数显式参与推导,否则宽泛的 string[] 会削弱品牌效果;
  • 默认数据类型是聚合形态:经 infiniteQueryOptions 处理后,读写缓存时操作的是 InfiniteDatapages/pageParams)而非单页数据,这与无限查询"N 页共享一个缓存条目"的模型一致(参见 InfiniteData 类型定义);
  • 错误类型可全局定制:默认错误为 Error,如需自定义,可通过 query-core 的 Register 接口增补 defaultError,或显式传入 TError 泛型。

小结

infiniteQueryOptions 用一行"原样返回"的运行时实现,为 Lit 应用中的无限查询换来了整条数据链路(控制器渲染、指令式翻页、缓存读写、命令式查询)上的一致且精确的类型推导。它把"这份缓存长什么样"这一信息封印在 queryKey 上,让 TanStack Query 的各条 API 都能"认出"彼此——这正是无限查询场景下减少 unknown、消除手写泛型的心智负担、并让乐观更新与缓存维护变得可编译校验的关键工具。它的实现与配套测试可分别在 packages/lit-query/src/infiniteQueryOptions.tstype-inference.test.tsinfinite-and-options.test.ts 中继续深入研究。

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

项目优选

收起
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