☰
Solid Query 与 TypeScript:类型推断、类型收窄与全局类型注册完整指南
2026/10/16 12:16:10 网站建设 项目流程

Solid Query 与 TypeScript:类型推断、类型收窄与全局类型注册完整指南

【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query

Solid Query(@tanstack/solid-query)本身使用TypeScript编写,其目标不仅是让库自身类型安全,更要让你的 Solid 项目在查询(query)与变更(mutation)的每个环节都获得完整的类型保障。本文以官方 TypeScript 指南为骨架,结合仓库内 solid-query 源码 与 query-core 核心类型,系统讲解类型推断的自动流转、基于可辨识联合的类型收窄、错误类型定制、全局Meta注册、queryOptions的类型安全提取,以及skipToken的按条件禁用方案。读完本文,你将能够在不手写冗余泛型的情况下,让data、error、queryKey等一切类型的推导都精确到业务类型。

版本支持与类型演进约定

在使用 TypeScript 之前,先了解 Solid Query 对 TypeScript 版本的承诺:

  • TanStack Query 遵循 DefinitelyTyped 的支持窗口,支持最近2 年内发布的 TypeScript 版本,目前意味着TypeScript 5.6 及更新版本;
  • 仓库中对类型的改动被视为非破坏性变更(non-breaking),通常以patch 级别的 semver 发布——否则每一次类型增强都会变成一次 major 升级;
  • 因此官方强烈建议将solid-query的包版本锁定到某个具体的 patch 版本,并在升级时预期"任意两个版本之间类型可能被修复或增强";
  • 与此同时,Solid Query 中与类型无关的公开 API仍然严格遵循 semver 规范。

这一点从源码中的默认类型也能印证:@tanstack/query-core的DefaultError类型通过条件类型解析(见 packages/query-core/src/types.ts#L45-L49),未注册任何自定义错误类型时回退为Error,类型体系被刻意设计为可扩展、可演进。

类型推断:让类型自动流转

Solid Query 的类型设计让大多数场景无需手写类型注解,类型会沿着queryFn的返回类型自动推导。

基础推导

import { useQuery } from '@tanstack/solid-query' const query = useQuery(() => ({ queryKey: ['number'], queryFn: () => Promise.resolve(5), })) query.data // ^? (property) data: number | undefined

这里queryFn返回Promise<number>,因此query.data被推断为number | undefined(undefined代表数据尚未加载完成)。

select 改变推导结果

使用select对数据进行投影时,data的类型会同步变为投影函数返回的类型:

import { useQuery } from '@tanstack/solid-query' const query = useQuery(() => ({ queryKey: ['test'], queryFn: () => Promise.resolve(5), select: (data) => data.toString(), })) query.data // ^? (property) data: string | undefined

queryFn 返回 any 时的对策

类型推导在queryFn返回类型明确时效果最好。注意:大多数数据请求库默认返回any(例如axios的response.data),此时推导会退化为any,因此建议把请求逻辑提取为返回类型明确的独立函数:

const fetchGroups = (): Promise<Group[]> => axios.get('/groups').then((response) => response.data) const query = useQuery(() => ({ queryKey: ['groups'], queryFn: fetchGroups, })) query.data // ^? (property) data: Group[] | undefined

源码层面的泛型设计

从 useQuery.ts 可以看到useQuery的泛型签名:TQueryFnData、TError、TData、TQueryKey,并且针对是否传入initialData提供了两组重载:

  • 无initialData时返回UseQueryResult<TData, TError>(data可为undefined);
  • 有initialData时返回DefinedUseQueryResult<TData, TError>(data被定义为非undefined)。

这些结果类型定义在 packages/solid-query/src/types.ts 中,均派生自@tanstack/query-core的QueryObserverResult/DefinedQueryObserverResult。这也是为什么大多数情况下你不需要手动传入泛型——库已经把"有无初始数据"这种细节编码进了重载签名里。

类型收窄:基于 status 的可辨识联合

Solid Query 的查询结果使用可辨识联合类型(discriminated union),判别字段是status(及其派生的布尔标志,如isSuccess、isLoading、isError)。利用这一点,你可以在检查success状态后让data变为确定非空的类型:

const query = useQuery(() => ({ queryKey: ['number'], queryFn: () => Promise.resolve(5), })) if (query.isSuccess) { const data = query.data // ^? const data: number }

这种设计让你在不引入额外断言(如!或as)的前提下安全消费数据,是 Solid Query 类型体验的核心一环。

错误字段的类型定制

默认行为:Error

错误字段的类型默认为Error,因为这符合大多数使用者的预期:

const query = useQuery(() => ({ queryKey: ['groups'], queryFn: fetchGroups, })) query.error // ^? (property) error: Error | null

通过泛型指定错误类型

如果你希望抛出自定义错误,甚至抛出完全不是Error的值,可以通过显式泛型指定:

const query = useQuery<Group[], string>(() => ({ queryKey: ['groups'], queryFn: fetchGroups, })) query.error // ^? (property) error: string | null

代价:一旦显式传入第一个泛型,useQuery其余泛型的类型推断将失效(需要全部手写)。而且,抛出非Error的值通常不被认为是好的实践。

用类型收窄保留 Error 的细化

如果你有AxiosError这样的Error子类,不要显式指定泛型,而是保留默认推断并用类型收窄来细化:

import axios from 'axios' const query = useQuery(() => ({ queryKey: ['groups'], queryFn: fetchGroups, })) query.error // ^? (property) error: Error | null if (axios.isAxiosError(query.error)) { query.error // ^? (property) error: AxiosError }

这样既能享受完整推断,又能在需要时获得更具体的错误类型。

全局注册默认错误类型

如果你希望全局改变defaultError(例如让所有error字段默认就是unknown,强制调用方显式收窄),可以通过模块增强注册:

import '@tanstack/solid-query' declare module '@tanstack/solid-query' { interface Register { // 使用 unknown,强制调用点显式收窄 defaultError: unknown } } const query = useQuery(() => ({ queryKey: ['groups'], queryFn: fetchGroups, })) query.error // ^? (property) error: unknown | null

这一机制的底层实现在 packages/query-core/src/types.ts#L37-L49:Register接口本身是空的(注释中标注了可注册的键),而DefaultError通过Register extends { defaultError: infer TError } ? TError : Error的条件类型解析,因此你在模块增强中注册什么类型,全仓库的默认错误类型就会变成什么。同样的模式也适用于queryKey、mutationKey、queryMeta、mutationMeta的全局定制。

注册全局 Meta 类型

与注册全局错误类型类似,你也可以注册全局Meta类型,确保 queries 与 mutations 上可选的meta字段保持一致且类型安全。注意:注册的类型必须继承Record<string, unknown>,以保证meta始终是一个对象:

import '@tanstack/solid-query' interface MyMeta extends Record<string, unknown> { // Your meta type definition. } declare module '@tanstack/solid-query' { interface Register { queryMeta: MyMeta mutationMeta: MyMeta } }

用 queryOptions 类型化地提取查询选项

把查询选项内联在useQuery中可以获得自动类型推断;但当你希望把选项提取成独立函数,以便在useQuery与queryClient.query()等场景间共享时,推断会丢失。此时使用queryOptions辅助函数可以把推断找回来:

import { queryOptions } from '@tanstack/solid-query' function groupOptions() { return queryOptions({ queryKey: ['groups'], queryFn: fetchGroups, staleTime: 5 * 1000, }) } useQuery(groupOptions) queryClient.query(groupOptions())

queryOptions更深层的价值在于:返回的queryKey携带了与其关联的queryFn数据类型,从而让queryClient.getQueryData等函数也能感知类型:

function groupOptions() { return queryOptions({ queryKey: ['groups'], queryFn: fetchGroups, staleTime: 5 * 1000, }) } const data = queryClient.getQueryData(groupOptions().queryKey) // ^? const data: Group[] | undefined

如果不使用queryOptions,data的类型会是unknown,除非你手动传入泛型:

const data = queryClient.getQueryData<Group[]>(['groups'])

底层原理:DataTag

queryOptions的实现位于 packages/solid-query/src/queryOptions.ts,其重载签名在返回类型上附加了QueryKeyWithDataTag<TQueryKey, TQueryFnData, TError>。这一类型的核心是@tanstack/query-core中基于 Symbol 的DataTag机制(见 packages/query-core/src/types.ts#L61-L100):queryKey被标记上dataTagSymbol与dataTagErrorSymbol,getQueryData等 API 再通过InferDataFromTag/InferErrorFromTag从 tagged key 中还原出数据类型。仓库中的类型测试 queryOptions.test-d.tsx 对该推断行为进行了回归验证,值得参考。

用 skipToken 类型安全地禁用查询

当你想按条件禁用查询,同时又希望保持完全类型安全时,可以使用skipToken。相比直接写enabled: false,skipToken被作为queryFn传入时,会向类型系统明确表达"这个查询当前不执行",并保持queryKey、data等类型不丢失:

import { createSignal } from 'solid-js' import { skipToken, useQuery } from '@tanstack/solid-query' function Todos() { const [filter, setFilter] = createSignal<string | undefined>() const todosQuery = useQuery(() => ({ queryKey: ['todos', filter()], // ⬇️ 当 filter 为 undefined 或空时,查询被禁用 queryFn: filter() ? () => fetchTodos(filter()!) : skipToken, })) return ( <div> {/* 🚀 应用 filter 后将启用并执行查询 */} <FiltersForm onApply={setFilter} /> <Show when={todosQuery.data}> <TodosTable data={todosQuery.data} /> </Show> </div> ) }

完整的使用场景(包括enabled布尔方式与skipToken方式的对比)可参考 Disabling Queries 指南,其中还演示了用Switch/Match处理data、isError、isLoading等状态的写法。skipToken在 infiniteQueryOptions.test-d.tsx 中也有对应的类型测试用例,说明它同样适用于useInfiniteQuery一族 API。

小结

Solid Query 的类型体系可以总结为三条主线:

  1. 自动推断:以queryFn的返回类型为源头,data、select结果、error等字段自动流转,配合initialData重载区分"已定义"与"可能为 undefined"的结果类型;
  2. 可辨识联合收窄:通过status/isSuccess等标志,在不写断言的情况下安全地获得非空数据;
  3. 全局与局部定制:借助Register接口的模块增强(defaultError、queryMeta、mutationMeta等)、queryOptions+ DataTag 的 tagged queryKey,以及skipToken,把类型安全延伸到团队级约定与条件禁用等复杂场景。

建议在实际项目中:将请求封装为返回类型明确的函数、优先使用queryOptions提取共享选项、把依赖的@tanstack/solid-query版本锁定到具体 patch,从而获得稳定、可演进、全程类型安全的开发体验。

【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询