@tanstack/lit-query 中 CreateQueriesInput 类型详解:createQueriesController 元组推断的基石
本文围绕 CreateQueriesInput 这一类型别名展开,结合 packages/lit-query/src/createQueriesController.ts 的源码实现,讲清楚它的四个泛型参数、它在 createQueriesController 元组推断管线中的位置,以及如何在 Lit 组件里用它写出完整类型安全的多查询列表。读完后,你将能够理解“传入查询选项元组 → 逐元素推断结果元组”的完整类型机制,并能处理 queryOptions 辅助函数、select、initialData、动态查询列表等进阶场景。
CreateQueriesInput 是什么
CreateQueriesInput 是 Lit Query 中 createQueriesController 的核心输入类型,其定义为:
type CreateQueriesInput<TQueryFnData, TError, TData, TQueryKey> = QueryObserverOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>;
该类型定义在 createQueriesController.ts,并通过 packages/lit-query/src/index.ts 从 @tanstack/lit-query 包公开导出。它的语义是:createQueriesController 中 queries 数组里“单个查询”的选项类型。
从源码注释看(L25-L30),它的设计意图有两条:
- 它镜像(mirrors)
@tanstack/query-core的QueryObserverOptions,因此所有单查询的选项(queryKey、queryFn、select、enabled、retry、staleTime等)都适用; - 它是“元组推断”(tuple inference)的原料——控制器接收一个查询选项元组后,会依据每个元素的
CreateQueriesInput类型逐元素推导出对应的结果类型,实现“第 i 个查询的选项决定第 i 个结果的结构”。
四个泛型参数及其默认值
参考文档列出了 CreateQueriesInput 的四个类型参数,与源码 L31-L36 完全一致:
| 参数 | 默认值 | 含义 |
|---|---|---|
TQueryFnData |
unknown |
queryFn 返回的原始数据类型 |
TError |
DefaultError |
查询失败时的错误类型 |
TData |
TQueryFnData |
最终暴露给消费者的数据类型(通常经 select 转换后与 TQueryFnData 不同) |
TQueryKey |
QueryKey(约束 extends QueryKey) |
queryKey 的类型 |
TData = TQueryFnData 这个默认值很关键:如果你没有写 select,结果里的 data 就是 queryFn 的返回值类型;一旦提供 select: (data) => ...,TData 会收敛为 select 的返回类型。这一点在源码的推断工具类型中体现为 unknown extends TData ? TQueryFnData : TData(L75、L125)——即“如果 TData 没有被推断出来,就回退为原始查询数据”。
需要说明的是:CreateQueriesInput 本身是“理论输入类型”。在实际传给 createQueriesController 时,用户写的是普通对象字面量或 queryOptions(...) 返回值,真正的泛型推导发生在下文的内部工具类型中。
推断管线:从输入元素到结果元素
CreateQueriesInput 被一组非导出的内部工具类型消费,整条管线都在 createQueriesController.ts 内。
1. CreateQueriesInputForController:控制器视角的输入
type CreateQueriesInputForController<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = OmitKeyof<CreateQueriesInput<TQueryFnData, TError, TData, TQueryKey>, never>
见 L38-L43。它基于 CreateQueriesInput 包了一层 OmitKeyof,是推断成功后统一使用的“归一化输入”类型。
2. GetCreateQueriesInput:识别用户写法的多个分支
GetCreateQueriesInput(L49-L78)负责从用户实际传入的元素中提取泛型参数,按优先级依次尝试:
queryOptions品牌化选项:若元素带有queryFnData/error/data标签(由 queryOptions 通过DataTag写入queryKey的品牌类型),直接取标签中的TQueryFnData、TError、TData。这就是推荐用法——先用queryOptions定义、再放入queries数组,类型能无损穿透;- 元组写法:依次匹配
[queryFnData, error, data]、[queryFnData, error]、[queryFnData]三种显式三元组/二元组,用于不需要实际执行queryFn的类型声明场景; - 普通对象写法:从
queryFn?: QueryFunction<infer TQueryFnData, infer TQueryKey> | SkipTokenForCreateQueries、select?: (data: any) => infer TData、throwOnError?: ThrowOnError<any, infer TError, any, any>三个位置推断泛型,并对TError、TData做unknown回退处理; - 兜底:以上都不匹配时返回无参的
CreateQueriesInputForController(即全默认泛型)。
这里还有一个值得注意的细节:SkipTokenForCreateQueries(L47)是一个专门的 symbol 类型,与 query-core 的 skipToken 语义对应,使得 queryFn: skipToken 写法在类型层面同样被接受。
3. 结果侧:GetCreateQueriesResult 与 Defined 判定
结果类型由 GetCreateQueriesResult(L100-L128)计算,内部再委托给 GetDefinedOrUndefinedCreateQueriesResult(L80-L98)。后者的核心逻辑是根据 initialData 判定结果是否为“已定义”:
- 若元素没有
initialData,结果为普通QueryObserverResult<TData, TError>(data可能为undefined); - 若
initialData存在且其类型(或惰性函数返回值)可赋值给TData,则升级为DefinedQueryObserverResult<TData, TError>(data保证非 undefined); - 其余情况(如
initialData是返回unknown的函数)回退为普通QueryObserverResult。
元组/对象两种写法走同一套判定,因此与输入侧的分支一一对应。
4. 元组递归:CreateQueriesOptions 与 CreateQueriesResults
最后一步是把“逐元素”的推断组织成整个数组:
CreateQueriesOptions(L130-L164):把输入元组T递归解构为[infer Head, ...infer Tails],对每个Head应用GetCreateQueriesInput,累积出归一化输入元组;CreateQueriesResults(L170-L186):同样的递归结构,用GetCreateQueriesResult生成结果元组。
两个递归都受 MAXIMUM_DEPTH = 20(L45)保护:当 TDepth['length'] extends MAXIMUM_DEPTH 时直接退化为 Array<CreateQueriesInputForController> / Array<QueryObserverResult>,避免超长元组导致的类型实例爆炸。
对于非元组的普通数组(如 array.map(...) 生成的 QueryObserverOptions[]),推断会走映射分支:输入侧统一为同构的 CreateQueriesInputForController<TQueryFnData, TError, TData, TQueryKey> 数组(L148-L163),结果侧则退化为索引映射 { [K in keyof T]: GetCreateQueriesResult<T[K]> }(L186)——即得到“元素类型的并集数组”,而非逐位置元组。
实战:在 Lit 组件中使用 createQueriesController
CreateQueriesInput 的完整选项集决定了 queries 中每个元素可以写什么。createQueriesController 的签名(L701-L719)接收三参:host、options(或返回 options 的 getter)、可选的 queryClient;返回可调用的 QueriesResultAccessor,带 current 属性与 destroy() 方法。
静态查询列表 + combine
源码 JSDoc 给出的官方示例(L676-L699)展示了最典型的用法:
import { LitElement, html } from 'lit'
import { createQueriesController } from '@tanstack/lit-query'
class DashboardView extends LitElement {
private readonly dashboard = createQueriesController(this, {
queries: [
{ queryKey: ['stats'], queryFn: fetchStats },
{ queryKey: ['projects'], queryFn: fetchProjects },
],
combine: ([stats, projects]) => ({
stats: stats.data,
projects: projects.data ?? [],
isPending: stats.isPending || projects.isPending,
}),
})
render() {
const dashboard = this.dashboard()
return html`<p>Projects: ${dashboard.projects.length}</p>`
}
}
其中 queries 里每个对象就是一组 CreateQueriesInput 实例;combine(CreateQueriesControllerOptions 的可选字段,L195-L210)接收推断出的结果元组,可把它重塑成任意单一值。
响应式查询列表(Accessor)
queries 与整个 options 都接受 Accessor<T> = T | (() => T)(定义见 accessor.ts)。传 getter 时,控制器会在宿主每次更新时重新读取查询列表,让查询数量/键跟随组件状态变化。queries-controller.test.ts 中的 DeferredFieldsQueriesHost 即为此模式:
readonly queries = createQueriesController(this, () => ({
queries: this.ids.map((id) => ({
queryKey: ['deferred-fields-queries', id] as const,
queryFn: async () => id,
retry: false,
})),
combine: (results) => results.map((result) => result.status),
}))
判断“是否需要在宿主更新时刷新”的逻辑见 shouldRefreshOnHostUpdate:options 是函数、或 options.queries 是函数时返回 true。
推荐配合 queryOptions 使用
若希望跨文件共享查询定义、且让 queryKey 携带数据/错误类型标签(DataTag),应先用 queryOptions 定义。其运行时实现就是原样返回入参(L141-L143),纯粹为类型服务;而 GetCreateQueriesInput 的第一个分支正是为识别这种品牌化输入而设,因此 queryOptions 产出的元素能以最高保真度参与元组推断。
运行时如何处理这些输入
类型推断之外,每个 CreateQueriesInput 元素在运行时的处理链路如下(均可在 createQueriesController.ts 中定位):
- 默认值合并:
resolveQueriesOptions(L287-L312)先通过readAccessor解析options与queries两个 getter,再对每个查询调用client.defaultQueryOptions(query),把客户端级默认选项合并进来,并打上_optimisticResults = 'optimistic'标记; - 观察器订阅:合并后的
QueryObserverOptions数组交给 query-core 的QueriesObserver,combine作为QueriesObserverOptions['combine']传入(L412-L417); - 占位结果:在
QueryClient尚未就绪(例如还没挂进QueryClientProvider子树)时,createPlaceholderQueryObserverResult(L257-L285)会为每个输入查询合成一个结果:没有initialData时是isPending: true的占位;有initialData时则直接以isSuccess: true呈现(同样会应用select与initialDataUpdatedAt)。测试 queries-controller.test.ts 的 LC-QUERIES-01 验证了“先读占位、Provider 连接后再取到真实数据”的完整时序; - 细粒度通知:
trackResult(L580-L602)把每个结果交由对应QueryObserver做属性级追踪,宿主只有在实际访问过的属性变化时才触发更新。
类型推断的测试证据
type-inference.test.ts 的 L1 用例用 expectTypeOf 固化了本文描述的推断行为:
- 元组保序推断:
queries: [{ queryFn: async () => 1 }, { queryFn: async () => 'x' }] as const的结果类型是[QueryObserverResult<number>, QueryObserverResult<string>],tupleData[0].data为number | undefined、tupleData[1].data为string | undefined; - combine 内逐位置可用:
combine: (result) => ({ first: result[0].data, second: result[1].data })中两个位置的类型均正确; - initialData 触发 Defined 结果:带
initialData: { id: 0, name: 'Seed' }的queryOptions元素,其data被推断为确定的{ id: number; name: string },而非| undefined; - 映射数组退化为并集:
[...numberQueries, booleanQuery]这种非元组数组中,mappedQueriesData[0].data为number | boolean | undefined,与上文的索引映射规则一致。
小结
CreateQueriesInput 虽然只是一个四参数别名,但它是 Lit Query 多查询控制器类型系统的入口:它镜像 QueryObserverOptions 保证了选项能力完整,而围绕它的 GetCreateQueriesInput、GetCreateQueriesResult、CreateQueriesOptions/CreateQueriesResults 递归(配合 MAXIMUM_DEPTH = 20 与 SkipTokenForCreateQueries)把“输入元组 → 结果元组”的逐位置推断变成了类型层面的自动完成。日常使用上,优先用 queryOptions 定义查询元素、需要响应式数量时用 getter 形式的 queries、需要单一返回值时用 combine,即可获得从选项到渲染数据的端到端类型安全。
参考路径
- 类型文档:docs/framework/lit/reference/type-aliases/CreateQueriesInput.md
- 类型定义与推断管线:packages/lit-query/src/createQueriesController.ts
- 包导出:packages/lit-query/src/index.ts
- queryOptions 品牌化辅助:packages/lit-query/src/queryOptions.ts
- Accessor 类型:packages/lit-query/src/accessor.ts
- 行为测试:packages/lit-query/src/tests/queries-controller.test.ts
- 类型推断测试:packages/lit-query/src/tests/type-inference.test.ts
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
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
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