深入 TanStack Query 的 lit-query:DefinedInitialDataOptions 类型别名与“确定非空”的查询数据保证
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、保证查询数据已定义的查询选项)。拆开看,它由两部分组成:
- 基座部分:
Omit<QueryObserverOptions<...>, "queryFn">。即以 query-core 中完整的QueryObserverOptions为基础,继承queryKey、enabled、select、staleTime、placeholderData、重试与缓存配置等所有观察者选项,但先把基类中的queryFn字段剔除,为后续重新声明腾出位置。 - 强化部分(交叉类型
&后补充的两个字段):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 的数据类型;而一旦在选项中提供了 select,TData 会独立推导为转换后的类型。
三、源码全景:三个选项类型与 queryOptions 的三重载
DefinedInitialDataOptions 并非孤立存在。从 queryOptions.ts 的源码结构看,整个文件围绕“initialData 的有无”划分出三个选项类型,并各自对应一个 queryOptions 工厂函数的重载:
DefinedInitialDataOptions(第 16 行):initialData必填且非 undefined —— 本文主角;UnusedSkipTokenOptions(第 34 行):queryFn存在且明确排除SkipToken | undefined,适用于“一定会发起请求”的场景;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
queryKeycarries 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 function(query.test.tsx)用vi.fn断言了initialDataFn恰好被调用 1 次; - 有初始数据时查询直接处于
success状态:status: hasData ? 'success' : 'pending',这正是 Lit/React 等框架中“带 initialData 的查询不显示 loading 态”的根源; dataUpdatedAt取initialDataUpdatedAt(可为值或函数,函数形式同样被调用,见 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(createQueryController、createQueriesController 等)接受 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 重载的查询,data 从 T | undefined 收紧为确定的 T。
此外,queryOptions 与命令式 API 的集成也在同一测试文件的 L3-L5 用例(第 249-283 行)中验证:queryOptions 的返回值可直接传给 client.query(options),select 会正确收窄 TData,enabled: false 时则要求已有缓存数据(这正与 initialData 提供的“确定初始状态”互为补充)。
七、要点小结
DefinedInitialDataOptions(packages/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 的
getDefaultState(packages/query-core/src/query.ts#L745)保证:函数形式initialData求值一次、有初始数据即进入success状态、初始数据参与staleTime过期计算,并有完整测试(packages/query-core/src/tests/query.test.tsx)固化这些行为; - 若你的场景是“一定会发请求、无初始数据”,应选用同文件的
UnusedSkipTokenOptions重载;“无初始数据”的常规场景则落到UndefinedInitialDataOptions。三者通过queryOptions的重载自动匹配,无需手动指定。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00