Svelte Query 的 CreateQueryResult 类型:createQuery 返回值结构、状态机与 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
CreateQueryResult<TData, TError>是 TanStack Query 为 Svelte 封装层(@tanstack/svelte-query)定义的查询结果类型,它描述createQuery的每一次返回值:包含status/fetchStatus双状态、data/error载荷以及一系列派生布尔标志。本文将以其定义(packages/svelte-query/src/types.ts#L47-L51)为骨架,深入 Svelte 源码与 query-core 类型层,讲清它的别名来源、泛型参数含义、底层结果联合类型、initialData对类型的收窄效果,以及如何在实际 Svelte 组件中正确消费这些字段——读完你就能熟练用类型安全的方式处理加载、成功、失败三种查询状态。
一、CreateQueryResult 的类型定义与定位
在 packages/svelte-query/src/types.ts 中,CreateQueryResult是一个极简的类型别名:
/** Result from createQuery */ export type CreateQueryResult< TData = unknown, TError = DefaultError, > = CreateBaseQueryResult<TData, TError>而CreateBaseQueryResult同样只是一层转发(同文件第 33-37 行):
/** Result from createBaseQuery */ export type CreateBaseQueryResult< TData = unknown, TError = DefaultError, > = QueryObserverResult<TData, TError>也就是说,Svelte 封装层自身没有定义新的结果结构,而是把类型责任完全委托给了 query-core 的QueryObserverResult。CreateQueryResult的存在意义在于:
- 提供语义化命名,明确"这是
createQuery的返回值",与createInfiniteQuery的CreateInfiniteQueryResult、createMutation的CreateMutationResult(同文件第 69-72、139-144 行)区分开; - 作为公开 API 的稳定类型出口,让组件、工具函数可以显式标注查询结果的类型,而不需要直接依赖 query-core 的内部类型名。
类型参数:TData 与 TError
CreateQueryResult接受两个泛型参数,均有默认值:
| 类型参数 | 默认值 | 含义 |
|---|---|---|
TData | unknown | 查询成功后data字段的实际数据类型,即queryFn解析值经select转换后的类型 |
TError | DefaultError | 查询失败时error字段的错误类型;DefaultError是 query-core 的默认错误类型(通常是Error) |
值得注意的是,TData的默认值是unknown而非TQueryFnData——在createQuery的签名中,TData默认继承TQueryFnData(见 packages/svelte-query/src/createQuery.ts#L74-L84),此时TData会被精确推断为queryFn的返回类型。但如果手动标注结果类型时没有传入TData,它就会退回到unknown,需要访问data前先收窄。
二、底层真相:QueryObserverResult 联合类型与结果状态机
要真正用好CreateQueryResult,必须理解它的底层别名QueryObserverResult。该类型定义在 packages/query-core/src/types.ts#L897-L902:
export type QueryObserverResult<TData = unknown, TError = DefaultError> = | DefinedQueryObserverResult<TData, TError> | QueryObserverLoadingErrorResult<TData, TError> | QueryObserverLoadingResult<TData, TError> | QueryObserverPendingResult<TData, TError> | QueryObserverPlaceholderResult<TData, TError>这是一个可辨识联合(discriminated union):每种成员都通过字面量status字段区分。五个分支分别是:
| 联合成员 | status | data | error | 关键标志 |
|---|---|---|---|---|
QueryObserverPendingResult | 'pending' | undefined | null | isPending: true |
QueryObserverLoadingResult | 'pending' | undefined | null | isPending: true、isLoading: true |
QueryObserverLoadingErrorResult | 'error' | undefined | TError | isLoadingError: true |
QueryObserverRefetchErrorResult | 'error' | TData | TError | isRefetchError: true |
QueryObserverSuccessResult | 'success' | TData | null | isSuccess: true |
QueryObserverPlaceholderResult | 'success' | TData | null | isPlaceholderData: true |
注意QueryObserverLoadingResult与QueryObserverPendingResult的status都是'pending',区别在于isLoading:前者代表首次加载进行中,后者是"禁用/未开始"状态。这也是 createQuery 文档中反复强调"禁用查询用isLoading而非isPending判断"的原因。
status 与 fetchStatus 双状态模型
从 packages/query-core/src/types.ts#L664-L665 可以看到 query-core 定义了双状态:
export type QueryStatus = 'pending' | 'error' | 'success' export type FetchStatus = 'fetching' | 'paused' | 'idle'status(QueryStatus)表示数据层面的状态:是否有可用数据、上次尝试是否失败;fetchStatus(FetchStatus)表示请求层面的状态:queryFn是否正在执行(fetching)、是否因网络模式被暂停(paused)或空闲(idle)。
两者组合才能完整描述一个查询:例如"有旧数据 + 后台刷新中"对应status: 'success'+fetchStatus: 'fetching'(isRefetching为true);"离线暂停"对应fetchStatus: 'paused'(isPaused为true)。
三、CreateBaseQueryResult 的完整字段清单
QueryObserverBaseResult(packages/query-core/src/types.ts#L667-L793)为所有联合成员提供了公共字段。这是CreateQueryResult实际携带的全部运行时信息,按用途分类如下:
数据与错误载荷
| 字段 | 类型 | 说明 |
|---|---|---|
data | TData \| undefined | 最后一次成功解析的数据;不同联合分支中被收窄为undefined或TData |
error | TError \| null | 查询抛出的错误对象,默认null |
dataUpdatedAt | number | status最近一次变为'success''的时间戳 |
errorUpdatedAt | number | status最近一次变为'error'的时间戳 |
errorUpdateCount | number | 所有错误的累计次数 |
failureCount | number | 失败次数,每次失败 +1,成功时重置为0 |
failureReason | TError \| null | 用于重试决策的失败原因,成功时重置为null |
派生布尔标志
| 字段 | 等价关系 | 说明 |
|---|---|---|
isPending | status === 'pending' | 无缓存数据且无已完成请求 |
isError | status === 'error' | 查询尝试出错 |
isSuccess | status === 'success' | 成功拿到数据,可渲染 |
isLoading | isFetching && isPending | 首次请求在途(不含禁用状态) |
isFetching | fetchStatus === 'fetching' | 任何请求在途,含后台刷新 |
isRefetching | isFetching && !isPending | 后台刷新在途 |
isLoadingError | — | 首次加载即失败(无旧数据) |
isRefetchError | — | 已有数据时刷新失败 |
isPlaceholderData | — | 当前展示的是 placeholder 数据 |
isPaused | fetchStatus === 'paused' | 想请求但被暂停 |
isStale | — | 缓存被失效或超过staleTime |
isFetched | — | 查询已被抓取过 |
isFetchedAfterMount | — | 组件挂载后是否抓取过(可用于屏蔽旧缓存) |
isEnabled | — | 观察者是否启用 |
isInitialLoading | — | 已废弃,改用isLoading |
状态与操作
status: QueryStatus——数据层状态('pending' | 'error' | 'success');fetchStatus: FetchStatus——请求层状态('fetching' | 'paused' | 'idle');refetch(options?)——手动重新抓取,返回Promise<QueryObserverResult<TData, TError>>,支持cancelRefetch等选项(见 packages/query-core/src/types.ts#L774-L776)。
四、源码链路:CreateQueryResult 是如何被生产出来的
理解类型后,再看运行时这条结果是如何诞生的。createQuery的实现(packages/svelte-query/src/createQuery.ts#L258-L263)极为简洁:
export function createQuery(options, queryClient?) { return createBaseQuery(options, QueryObserver, queryClient) }它把工作委托给createBaseQuery(packages/svelte-query/src/createBaseQuery.svelte.ts),核心流程为:
- 解析客户端:
$derived(useQueryClient(queryClient?.()))——默认从最近上下文取QueryClient; - 默认化选项:
client.defaultQueryOptions(options())合并默认配置,并根据isRestoring设置_optimisticResults; - 创建观察者:
new QueryObserver(client, resolvedOptions),并在客户端变化时重建; - 生成结果:
observer.getOptimisticResult(resolvedOptions)获得"乐观结果",再通过observer.trackResult(result)跟踪属性访问以实现细粒度响应式更新; - 订阅同步:
$effect中observer.subscribe(() => update(createResult())),观察者每次通知都重新计算结果。
由于 Svelte 5 的 runes 机制($derived、$state、$effect),options被设计为Accessor<T>(即() => T函数)以实现响应式——选项变化时watchChanges会调用observer.setOptions(resolvedOptions)触发重新计算。返回值就是类型为CreateQueryResult<TData, TError>的响应式结果。
测试印证:真实字段行为
packages/svelte-query/tests/createQuery/Base.svelte 是官方测试夹具,直接消费createQuery的返回值,把status、fetchStatus、data、isFetched、isStale、isFetching、isSuccess、isPlaceholderData等字段渲染到 DOM 供断言;同目录测试用例(如createQuery.svelte.test.ts)验证了isLoading/isPending/isLoadingError/isPlaceholderData等标志在加载、成功、错误各阶段的组合取值。这说明上述字段不仅是类型声明,更是经测试验证的运行时契约。
五、泛型推导与 initialData 的类型收窄
CreateQueryResult有一个关键类型行为:当且仅当传入initialData时,结果类型会被收窄为DefinedCreateQueryResult,此时data不再是TData | undefined而是保证存在的TData。
createQuery在 packages/svelte-query/src/createQuery.ts 中提供了三个重载:
// 重载 1:未设置 initialData function createQuery<TQueryFnData, TError, TData, TQueryKey>( options: Accessor<UndefinedInitialDataOptions<...>>, queryClient?: Accessor<QueryClient>, ): CreateQueryResult<TData, TError> // 重载 2:设置了 initialData —— 返回 DefinedCreateQueryResult function createQuery<TQueryFnData, TError, TData, TQueryKey>( options: Accessor<DefinedInitialDataOptions<...>>, queryClient?: Accessor<QueryClient>, ): DefinedCreateQueryResult<TData, TError> // 重载 3:通用(支持 select 等) function createQuery<TQueryFnData, TError, TData, TQueryKey>( options: Accessor<CreateQueryOptions<...>>, queryClient?: Accessor<QueryClient>, ): CreateQueryResult<TData, TError>DefinedCreateQueryResult的定义在 packages/svelte-query/src/types.ts#L87-L90:
export type DefinedCreateQueryResult< TData = unknown, TError = DefaultError, > = DefinedCreateBaseQueryResult<TData, TError>它最终对应 query-core 的DefinedQueryObserverResult(packages/query-core/src/types.ts#L890-L895),只包含QueryObserverRefetchErrorResult和QueryObserverSuccessResult两个分支——data在这两个分支中都是TData。类型层面还保证了status永远不会是'pending'(有initialData就有数据可展示),因此模板中可以不写 loading 分支。对应的DefinedInitialDataOptions/UndefinedInitialDataOptions类型定义在 packages/svelte-query/src/queryOptions.ts#L10-L28。
实践价值:当你在组件里写下createQuery(() => ({ queryKey: ['posts'], queryFn: fetchPosts, initialData: [] }))时,TypeScript 自动选择重载 2,query.data被推断为非空数组,{@each query.data as post}无需空值保护;而未传initialData时,data保持TData | undefined,模板需要先通过status/isPending分支收窄。
六、在 Svelte 组件中消费 CreateQueryResult
以下用法全部来自 createQuery 官方文档示例(docs/framework/svelte/reference/functions/createQuery.md)与源码 JSDoc 示例(packages/svelte-query/src/createQuery.ts),可直接复制运行。
1. 通过 status 分支渲染三态
<script lang="ts"> import { createQuery } from '@tanstack/svelte-query' const query = createQuery(() => ({ queryKey: ['posts'], queryFn: fetchPosts, })) </script> {#if query.status === 'pending'} Loading... {:else if query.status === 'error'} <span>Error: {query.error.message}</span> {:else} <ul> {#each query.data as post (post.id)} <li>{post.title}</li> {/each} </ul> {/if}这里status联合类型让 TS 自动收窄:error分支中query.error可用,success分支中query.data是Post[]。
2. 使用派生布尔标志
isPending/isSuccess/isError与status完全等价,选择可读性更好的写法:
{#if query.isPending} Loading... {:else if query.isError} <span>Error: {query.error.message}</span> {:else} <ul> {#each query.data as post (post.id)} <li>{post.title}</li> {/each} </ul> {/if}3. initialData 收窄类型、避免 loading 闪烁
<script lang="ts"> import { createQuery } from '@tanstack/svelte-query' // `data` 是 `Post[]`,绝不可能是 `undefined` —— 即使刷新失败, // 列表仍会与错误信息一起展示(status 不会进入 pending) const query = createQuery(() => ({ queryKey: ['posts'], queryFn: fetchPosts, initialData: [], })) </script> {#if query.isError} <span>Error: {query.error.message}</span> {/if} <ul> {#each query.data as post (post.id)} <li>{post.title}</li> {/each} </ul>4. select 派生 data,不改动缓存
select会在缓存值之上派生组件所需的数据,缓存中仍是完整Post[],但query.data的类型变为number:
<script lang="ts"> import { createQuery } from '@tanstack/svelte-query' const query = createQuery(() => ({ queryKey: ['posts'], queryFn: fetchPosts, select: (posts) => posts.length, })) </script> {#if query.isPending} Loading... {:else if query.isError} <span>Error: {query.error.message}</span> {:else} <span>{query.data} posts</span> {/if}5. 禁用查询用 isLoading 而非 isPending
enabled: false时status为pending,但不应显示 loading。isLoading === isFetching && isPending,禁用状态下两者皆假:
<script lang="ts"> import { createQuery } from '@tanstack/svelte-query' let { postId }: { postId: number | undefined } = $props() const query = createQuery(() => ({ queryKey: ['post', postId], queryFn: () => fetchPost(postId!), enabled: postId != null, })) </script> {#if postId == null} Select a post {:else if query.isLoading} Loading... {:else if query.isError} <span>Error: {query.error.message}</span> {:else} <h1>{query.data?.title}</h1> {/if}6. 用缓存列表为详情查询做 initialData 种子
从已缓存的列表查询中寻找详情数据作为initialData,跳过详情页的加载态:
<script lang="ts"> import { createQuery, useQueryClient } from '@tanstack/svelte-query' let { postId }: { postId: number } = $props() const queryClient = useQueryClient() const query = createQuery(() => ({ queryKey: ['post', postId], queryFn: () => fetchPost(postId), initialData: () => queryClient .getQueryData<Array<Post>>(['posts']) ?.find((post) => post.id === postId), })) </script> {#if query.isError} <span>Error: {query.error.message}</span> {/if} <h1>{query.data?.title}</h1>7. placeholderData 与 isPlaceholderData:翻页时保留旧数据
分页查询中placeholderData: keepPreviousData让上一页数据在下一页加载期间继续可见,isPlaceholderData用于禁用按钮:
<script lang="ts"> import { createQuery, keepPreviousData } from '@tanstack/svelte-query' let page = $state(0) const query = createQuery(() => ({ queryKey: ['posts', page], queryFn: () => fetchPosts(page), placeholderData: keepPreviousData, })) </script> {#if query.isError} <span>Error: {query.error.message}</span> {/if} <ul> {#each query.data ?? [] as post (post.id)} <li>{post.title}</li> {/each} </ul> <button disabled={query.isPlaceholderData} onclick={() => page++}> Next Page </button>七、与其他结果类型的对应关系
CreateQueryResult不是孤立存在的——types.ts中还定义了一组姊妹类型,便于按查询形态选择正确的结果类型:
| Svelte 封装类型 | 对应核心类型 | 适用场景 |
|---|---|---|
CreateBaseQueryResult | QueryObserverResult | createBaseQuery的通用结果 |
CreateQueryResult | QueryObserverResult | createQuery的结果 |
DefinedCreateQueryResult | DefinedQueryObserverResult | createQuery且设置了initialData |
CreateInfiniteQueryResult | InfiniteQueryObserverResult | createInfiniteQuery的结果 |
DefinedCreateInfiniteQueryResult | DefinedInfiniteQueryObserverResult | createInfiniteQuery且设置了initialData |
CreateMutationResult | MutationObserverResult(含重写的mutate/mutateAsync) | createMutation的结果 |
例如createInfiniteQuery返回CreateInfiniteQueryResult<TData, TError>(packages/svelte-query/src/types.ts#L69-L72),其底层InfiniteQueryObserverBaseResult在QueryObserverBaseResult基础上额外增加了data(页数组)、hasNextPage/hasPreviousPage、fetchNextPage/fetchPreviousPage、isFetchingNextPage/isFetchingPreviousPage等分页字段。
八、实战排查:结果字段不符合预期时的检查清单
当你发现query的某个字段行为异常时,按以下顺序排查:
- 确认响应式写法:
createQuery的 options 必须是Accessor(() => ({...})),而不是普通对象——Svelte 5 下才能追踪响应式依赖(见 packages/svelte-query/src/createBaseQuery.svelte.ts 中的watchChanges机制); - 区分 status 与 fetchStatus:
isPending看数据层,isFetching看请求层;"有旧数据在刷新"时status === 'success'但isFetching === true; - 禁用查询:
enabled: false时不要用isPending判断 loading,用isLoading; - 手动 refetch:
query.refetch()返回 Promise,可用await获取刷新后的结果,配合cancelRefetch: false可避免取消在途请求(见 packages/svelte-query/tests/createQuery/Base.svelte 中的按钮绑定); - 类型收窄失效:确认是否传了
initialData;若用queryOptions工厂共享配置,注意queryOptions也会保留initialData的类型标记(packages/svelte-query/src/queryOptions.ts)。
结语
CreateQueryResult<TData, TError>虽然只是一行类型别名,但它连接着 Svelte 响应式层与 query-core 的完整查询状态机。掌握它的联合分支结构(pending/error/success×fetching/paused/idle)、字段语义与initialData收窄规则,你就能写出类型安全、状态判断准确的 Svelte Query 组件——无论是简单列表、依赖查询、分页还是乐观 UI,都能基于这一份结果契约从容实现。
【免费下载链接】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),仅供参考