首页
/ 深入 TanStack Query 的 lit-query:DefinedInitialDataOptions 类型别名与“确定非空”的查询数据保证

深入 TanStack Query 的 lit-query:DefinedInitialDataOptions 类型别名与“确定非空”的查询数据保证

2026-09-07 17:05:03作者:韦蓉瑛

DefinedInitialDataOptions 是 @tanstack/lit-query 中一个承上启下的类型别名:它表示“携带了 initialData 的查询选项”,并在 TypeScript 层面保证查询数据永远是已定义的(TData 而非 TData | undefined)。本文以 DefinedInitialDataOptions 官方参考文档 为主体,结合 packages/lit-query/src/queryOptions.ts 的完整源码与 @tanstack/query-core 中 initialData 的实际消费逻辑,讲清这个类型的结构、泛型参数、与 queryOptions 工厂函数的重载关系,以及它在 Lit 控制器场景下如何落地为可编译的类型保证。

一、类型声明:去掉 queryFn 可选性、强化 initialData 的选项类型

参考文档给出的类型声明如下,其定义位置为 packages/lit-query/src/queryOptions.ts 第 16 行

type DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey> = Omit<QueryObserverOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>, "queryFn"> & {
  initialData: NonUndefinedGuard<TQueryFnData> | (() => NonUndefinedGuard<TQueryFnData>),
  queryFn?: QueryFunction<TQueryFnData, TQueryKey>
}

官方注释一句话概括了它的职责:“Query options with initialData that guarantees defined query data.”(携带 initialData、保证查询数据已定义的查询选项)。拆开看,它由两部分组成:

  1. 基座部分Omit<QueryObserverOptions<...>, "queryFn">。即以 query-core 中完整的 QueryObserverOptions 为基础,继承 queryKeyenabledselectstaleTimeplaceholderData、重试与缓存配置等所有观察者选项,但先把基类中的 queryFn 字段剔除,为后续重新声明腾出位置。
  2. 强化部分(交叉类型 & 后补充的两个字段):
    • initialData必填,且取值只能是 NonUndefinedGuard<TQueryFnData> 本身,或一个返回该类型的函数 () => NonUndefinedGuard<TQueryFnData>NonUndefinedGuard 会把 undefined 从类型中剔除(见下文第四节),因此这里在编译期就断言了“初始数据绝不是 undefined”。
    • queryFn?可选QueryFunction<TQueryFnData, TQueryKey>。注意与基类的差别——在“有初始数据”的语义下,queryFn 被重新声明为可选,允许你只提供 initialData 而依赖缓存/预取来填充真实数据。

文档中“Type Declaration”一节列出的两个成员与源码完全一致:

initialData:
  | NonUndefinedGuard<TQueryFnData>
  | () => NonUndefinedGuard<TQueryFnData>;

optional queryFn: QueryFunction<TQueryFnData, TQueryKey>;

这种“必填 initialData + 可选 queryFn”的组合,正是它与同文件内另外两个兄弟类型区分开来的关键,下面展开。

二、类型参数与默认值

参考文档的 “Type Parameters” 一节给出了 4 个泛型参数及其默认值,与源码签名一一对应:

类型参数 默认值 含义
TQueryFnData unknown queryFn 返回的原始数据类型
TError DefaultError 查询失败时的错误类型
TData TQueryFnData 观察者对外暴露的数据类型(可被 select 转换)
TQueryKey QueryKey(且 extends QueryKey 查询键类型

packages/lit-query/src/queryOptions.ts 中,签名写作:

export type DefinedInitialDataOptions<
  TQueryFnData = unknown,
  TError = DefaultError,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
> = ...

注意 TData = TQueryFnData 这一默认推导:如果你不显式指定转换类型,暴露的数据类型就等于 queryFn 的数据类型;而一旦在选项中提供了 selectTData 会独立推导为转换后的类型。

三、源码全景:三个选项类型与 queryOptions 的三重载

DefinedInitialDataOptions 并非孤立存在。从 queryOptions.ts 的源码结构看,整个文件围绕“initialData 的有无”划分出三个选项类型,并各自对应一个 queryOptions 工厂函数的重载:

  1. DefinedInitialDataOptions(第 16 行):initialData 必填且非 undefined —— 本文主角;
  2. UnusedSkipTokenOptions(第 34 行):queryFn 存在且明确排除 SkipToken | undefined,适用于“一定会发起请求”的场景;
  3. UndefinedInitialDataOptions(第 58 行):initialData 可省略或为 undefined,是最宽泛的常规选项类型。

queryOptions 函数则按“定义顺序敏感”的三个重载签名依次匹配这三个类型(第 94、112、130 行),运行时实现只有一个恒等函数(第 141 行):

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

也就是说 queryOptions 在运行时是零开销的透传,它的全部价值在于类型层面

  • 依据你传入的选项形状,TS 会选中对应重载,从而决定返回类型中 queryKey 的“品牌”;
  • 每个重载的返回值都会在选项类型上交叉一个 queryKey: DataTag<TQueryKey, TQueryFnData, TError>,即把数据与错误类型“打标签”在查询键上。源码注释原话是:

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

文档给出的官方示例正出自该文件的 JSDoc(第 83-92 行):

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

const todosOptions = queryOptions({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  initialData: [],
})

提供 initialData: [] 会命中 DefinedInitialDataOptions 重载,此后 todosOptions 在传给 Lit 的查询控制器时,data 的类型就不再携带 undefined

四、类型支撑:NonUndefinedGuard 与 DataTag

DefinedInitialDataOptions 的“非空保证”依赖 query-core 导出在 packages/query-core/src/types.ts 的两个类型工具:

NonUndefinedGuard(第 12 行)——一个条件类型,把 undefined 从类型中剔除:

export type NonUndefinedGuard<T> = T extends undefined ? never : T

这保证了 initialData 无论取值还是取函数形式,都不可能把 undefined 作为“初始数据”传入,从类型层面堵住“声称有初始数据但实际为 undefined”的错误。

DataTag(第 67-80 行)——用两个 Symbol(dataTagSymbol / dataTagErrorSymbol)在 queryKey 类型上“隐形”地挂载 TValue(数据)与 TError

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

正因为这个品牌机制,queryOptions 的返回值可以无缝接入 QueryClient.getQueryData / setQueryData 等 API 并获得精确的数据类型——这是 query-core 的类型测试 中专门验证过的行为。

五、运行时语义:initialData 在 query-core 中如何被消费

类型只是编译期承诺,真正的行为发生在 query-core 的默认状态构造逻辑里。packages/query-core/src/query.ts 的 getDefaultState 完整对应了 DefinedInitialDataOptions 的两种 initialData 形式:

const data =
  typeof options.initialData === 'function'
    ? (options.initialData as InitialDataFunction<TData>)()
    : options.initialData

const hasData = data !== undefined

const initialDataUpdatedAt = hasData
  ? typeof options.initialDataUpdatedAt === 'function'
    ? options.initialDataUpdatedAt()
    : options.initialDataUpdatedAt
  : 0

return {
  data,
  dataUpdatedAt: hasData ? (initialDataUpdatedAt ?? Date.now()) : 0,
  status: hasData ? 'success' : 'pending',
  fetchStatus: 'idle',
  // ...
}

可以归纳出三条与“defined data”直接相关的运行时事实:

  • 函数形式在 Query 构造时立即求值一次:query-core 的测试 should call initialData function when it is a functionquery.test.tsx)用 vi.fn 断言了 initialDataFn 恰好被调用 1 次;
  • 有初始数据时查询直接处于 success 状态status: hasData ? 'success' : 'pending',这正是 Lit/React 等框架中“带 initialData 的查询不显示 loading 态”的根源;
  • dataUpdatedAtinitialDataUpdatedAt(可为值或函数,函数形式同样被调用,见 query.test.tsx 第 1021 行 的测试)或 Date.now(),因此初始数据会参与 staleTime 的过期计算,而非被当作“永不过期”的数据。

另外,resetQueries 等回退操作会回到 initialData(测试见 query.test.tsx 第 1368 行附近should update initialData when Query exists without data),进一步印证 DefinedInitialDataOptions 的语义:initialData 是这条查询的“兜底事实数据”。

六、在 Lit 场景中的落地:类型推断验证

lit-query 的控制器 API(createQueryControllercreateQueriesController 等)接受 queryOptions 的返回值作为选项参数。仓库中的类型推断测试 packages/lit-query/src/tests/type-inference.test.ts 给出了两条与 DefinedInitialDataOptions 直接相关的实证:

1. 单控制器场景(L2 用例,第 178-196 行)——queryOptions 返回值的 queryKey 被品牌化,QueryClient 的读写 API 获得精确类型:

const queryOpts = queryOptions({
  queryKey: ['type-inference', 'query-options'] as const,
  queryFn: async () => ({ id: 2, name: 'Grace' }),
})
expectTypeOf(queryOpts.queryKey[dataTagSymbol]).toEqualTypeOf<{
  id: number
  name: string
}>()
const cachedData = client.getQueryData(queryOpts.queryKey)
expectTypeOf(cachedData).toEqualTypeOf<{ id: number; name: string } | undefined>()

2. 多控制器 + initialData 场景(L1 用例,第 81-101 行)——这是“defined data 保证”最直观的验证:提供 initialData 后,结果元组里 data 的类型不含 undefined

const definedInitialDataResult = createQueriesController(
  host,
  {
    queries: [
      queryOptions({
        queryKey: ['type-inference', 'defined-initial-data'] as const,
        queryFn: async () => ({ id: 4, name: 'Marie' }),
        initialData: { id: 0, name: 'Seed' },
      }),
    ] as const,
  },
  client,
)

const definedInitialDataTuple = expectDefinedInitialDataTuple(
  definedInitialDataResult(), // 形参要求 [DefinedQueryObserverResult<{ id: number; name: string }>]
)
expectTypeOf(definedInitialDataTuple[0].data).toEqualTypeOf<{
  id: number
  name: string
}>()

对照同文件第 54 行的普通查询 expectTypeOf(tupleData[0].data).toEqualTypeOf<number | undefined>(),差异一目了然:命中 DefinedInitialDataOptions 重载的查询,dataT | undefined 收紧为确定的 T

此外,queryOptions 与命令式 API 的集成也在同一测试文件的 L3-L5 用例(第 249-283 行)中验证:queryOptions 的返回值可直接传给 client.query(options)select 会正确收窄 TDataenabled: false 时则要求已有缓存数据(这正与 initialData 提供的“确定初始状态”互为补充)。

七、要点小结

  • DefinedInitialDataOptionspackages/lit-query/src/queryOptions.ts#L16)= QueryObserverOptions 去掉 queryFn 后,叠加“必填且非 undefined 的 initialData(值或函数)+ 可选 queryFn”,是 lit-query 三套选项类型中最“确定”的一种;
  • 4 个泛型参数 TQueryFnData(默认 unknown)、TError(默认 DefaultError)、TData(默认 TQueryFnData)、TQueryKey(默认 QueryKey)与官方参考文档逐条对应;
  • 它的实际效果由 queryOptions 的重载 + DataTag 品牌化实现:运行时是恒等透传,价值全在编译期——命中该重载的查询,其 data 不含 undefined,且 queryKey 可反哺 getQueryData/setQueryData 的类型推断;
  • 运行时侧,query-core 的 getDefaultStatepackages/query-core/src/query.ts#L745)保证:函数形式 initialData 求值一次、有初始数据即进入 success 状态、初始数据参与 staleTime 过期计算,并有完整测试(packages/query-core/src/tests/query.test.tsx)固化这些行为;
  • 若你的场景是“一定会发请求、无初始数据”,应选用同文件的 UnusedSkipTokenOptions 重载;“无初始数据”的常规场景则落到 UndefinedInitialDataOptions。三者通过 queryOptions 的重载自动匹配,无需手动指定。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

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