news 2026/9/9 19:04:38

深入解读 Preact Query 的 DefinedUseQueryResult:`data` 永不为 `undefined` 的类型保证

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入解读 Preact Query 的 DefinedUseQueryResult:`data` 永不为 `undefined` 的类型保证

深入解读 Preact Query 的 DefinedUseQueryResult:data永不为undefined的类型保证

【免费下载链接】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

DefinedUseQueryResult@tanstack/preact-query为查询结果提供的一类“确定型”类型别名:当你在useQuery中设置initialData(或使用useSuspenseQuery)时,TypeScript 会自动把返回值收紧为该类型,从类型层面保证解构出来的data绝不可能是undefined。本文将以 DefinedUseQueryResult 类型参考文档 为主线,结合本仓库中preact-queryquery-core的源码实现,讲清它的定义、触发条件、底层结构与实战价值,帮助你在 Preact 应用中编写更安全的取数代码,彻底告别手动非空断言。

一、DefinedUseQueryResult 的类型定义一览

该类型别名在 preact-query/src/types.ts:352 中定义,完整声明只有一行:

export type DefinedUseQueryResult< TData = unknown, TError = DefaultError, > = DefinedQueryObserverResult<TData, TError>

从文档注释与源码注释可以提炼出它的两条关键语义:

  1. 它是useQueryinitialData被设置时的返回值类型——此时data永远不会是undefined
  2. 它也是useSuspenseQuery省略isPlaceholderData字段之前的基础形态——因为 Suspense 场景下该字段恒为false,是一个“死字段”,所以被裁剪掉而非保留为活跃状态。

从本质上说,DefinedUseQueryResult并不是一个新的类型体系,它只是把@tanstack/query-core中的DefinedQueryObserverResult原样re-export(再导出)preact-query的门面层,让框架使用者无需直接依赖query-core也能引用该结果类型。

二、它解决的核心问题:让data可被安全解构

普通useQuery(未设置initialData)的返回值类型是 UseQueryResult(本质上是UseBaseQueryResult,即 query-core 的QueryObserverResult)。在请求尚未完成时,查询处于pending状态,此时的dataundefined。因此,普通用法下 TypeScript 会认为data可能为空,访问data内容前必须做判空。

而一旦配置了initialData,查询在一开始就有可展示的数据,data从类型上就不会为undefined。对应的,useQuery的重载签名返回的就是DefinedUseQueryResult——你甚至可以直接对data调用.map、访问属性,而无需可空性检查,编译器会为你背书。

这一“有无initialData→ 不同返回类型”的设计,在 useQuery.ts 的重载签名上体现得淋漓尽致:

// packages/preact-query/src/useQuery.ts // 重载一:options 带 initialData(DefinedInitialDataOptions) export function useQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>, queryClient?: QueryClient, ): DefinedUseQueryResult<TData, TError> // ← data 非空 // 重载二:options 不带 initialData(UndefinedInitialDataOptions) export function useQuery< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( options: UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>, queryClient?: QueryClient, ): UseQueryResult<TData, TError> // ← data 可能为 undefined

真正的实现只有一行——它把上述重载统一委托给useBaseQuery(options, QueryObserver, queryClient)(见 useQuery.ts 的最终实现),也就是说,“类型是否确定”是编译期由 options 形状(是否含initialData)推断出来的,运行期并不存在两套逻辑。

条件分支同样出现在 useQueries 中

这种按initialData是否存在来选择返回类型的逻辑,在批量查询 useQueries.ts 中同样存在。从源码注释 "A defined initialData setting should return a DefinedUseQueryResult rather than UseQueryResult" 可以看到,useQueries也会在检测到某项initialData被设置时,将该项结果的联合类型成员收紧为DefinedUseQueryResult<TData, TError>

三、底层来源:query-core 的 DefinedQueryObserverResult

DefinedUseQueryResult被 re-export 自 query-core,其底层联合类型定义在 packages/query-core/src/types.ts:890-L896:

export type DefinedQueryObserverResult< TData = unknown, TError = DefaultError, > = | QueryObserverRefetchErrorResult<TData, TError> | QueryObserverSuccessResult<TData, TError>

与之相对的,普通QueryObserverResult则是一个包含五种“状态子类型”的联合:

export type QueryObserverResult<TData = unknown, TError = DefaultError> = | DefinedQueryObserverResult<TData, TError> | QueryObserverLoadingErrorResult<TData, TError> | QueryObserverLoadingResult<TData, TError> | QueryObserverPendingResult<TData, TError> | QueryObserverPlaceholderResult<TData, TError>

对比可以看出:DefinedQueryObserverResult只保留了五种状态子类型中的两种——refetch 失败但保留旧数据(QueryObserverRefetchErrorResult)与完全成功(QueryObserverSuccessResult)。这正是“确定性”的由来:其余三种(首次加载失败、首次加载中、完全 pending)都对应着data === undefined的窗口期,被initialData直接抹平了。

这两种成员接口的关键字段窄化情况如下:

状态子类型dataerrorisPendingstatusisPlaceholderData
QueryObserverRefetchErrorResult(types.ts:842)TData(保留旧数据)TErrorfalse'error'false
QueryObserverSuccessResult(types.ts:858)TDatanullfalse'success'false

从 DefinedUseQueryResult 中能解构出哪些字段

因为DefinedQueryObserverResult是联合类型,DefinedUseQueryResult的结果对象继承了QueryObserverBaseResult的全部字段(见 packages/query-core/src/types.ts 附近的完整定义),主要包括:

  • data: TData——确定存在的数据,这是核心保证;
  • errorerrorUpdateCount——success分支下errornullrefetchError分支下为TError
  • status(联合窄化为'success' | 'error')以及派生布尔量isSuccess/isError/isPending/isLoading/isFetching/isRefetching/isStale/isPaused等;
  • fetchStatus'idle' | 'fetching' | 'paused');
  • refetch(options?)手动重取函数。

注意:即便带initialData,也仍可能发生后台 refetch 失败。此时结果属于QueryObserverRefetchErrorResult分支:status === 'error'error有值,但data依然保留旧值。因此“data非空”与“请求一定成功”是两回事——类型保证的是前者,UI 上你依然需要决定是否展示error(通常做法是照常渲染列表,同时用一条提示展示错误)。

四、与相邻结果类型的亲缘关系

在 types.ts 中,围绕结果类型有一组成体系的别名,理解它们的位置有助于精准把握DefinedUseQueryResult的角色:

类型别名对应 hook底层类型data是否可能为undefined备注
UseQueryResult(types.ts:324)useQuery(无initialDataQueryObserverResult可能UseBaseQueryResult
DefinedUseQueryResult(types.ts:352)useQuery+initialDataDefinedQueryObserverResult不会本文主角
UseSuspenseQueryResult(types.ts:336)useSuspenseQueryDistributiveOmit<DefinedQueryObserverResult, 'isPlaceholderData'>不会DefinedUseQueryResult基础上剔除isPlaceholderData
UseInfiniteQueryResult/DefinedUseInfiniteQueryResult(types.ts:364 / types.ts:376)useInfiniteQuery/ 带initialDataInfiniteQueryObserverResult/DefinedInfiniteQueryObserverResult视是否带initialDatainfinite 场景的对应物

值得展开的是文档中提到的 “ofuseSuspenseQuerybeforetheisPlaceholderDataomission” 这句表述。其源码实现为:

export type UseSuspenseQueryResult< TData = unknown, TError = DefaultError, > = DistributiveOmit< DefinedQueryObserverResult<TData, TError>, 'isPlaceholderData' >

也就是说,useSuspenseQuery的结果类型正是DefinedUseQueryResult的底层联合上做了一次“分布式剔除(DistributiveOmit)”,把在所有分支中恒为falseisPlaceholderData字段从类型上删掉。它的意义是双向的:既继承了data非空的确定性,又避免了让使用者误以为在 Suspense 场景下还会渲染占位数据。这层“先 Defined、后剔除死字段”的组合关系,恰恰解释了为什么文档把它描述为useSuspenseQuery“剔除isPlaceholderData之前”的形态。

五、泛型参数说明

DefinedUseQueryResult<TData, TError>携带两个类型参数,默认值与含义如下:

TData(默认unknown

  • 含义data在经过select转换之后呈现出的类型。
  • 说明:若没有使用select,框架会把TData默认推断为TQueryFnData(即queryFn的返回类型);一旦配置了select: (data) => derivedDatadata的类型将变成select的返回值类型,而query cache 中存储的仍是未经转换的原始数据(输入selectTQueryData保持不变)。

TError(默认DefaultError

  • 含义queryFn可能抛出的错误类型。DefaultError是 query-core 提供的默认错误类型,通常会被自定义错误类或unknown覆盖。
  • 说明:当你只关心成功数据、不渲染错误细节时,通常无需显式指定该参数,交由推断即可;需要把error与自定义错误类关联时,再显式传入。

类型参数使用的完整示例

useQuery场景里,TDataTError通常是从 options 中自动推断的,但把它们显式用于注解“收集状态的数组”时,能直观体现结果的确定性,例如仓库测试中常见的写法(见 preact-query 的 useQuery 测试):

import { useQuery } from '@tanstack/preact-query' import type { DefinedUseQueryResult } from '@tanstack/preact-query' // 显式声明:states 里每一项的 data 都是 number,而非 number | undefined const states: Array<DefinedUseQueryResult<number>> = [] function Page() { const [count, setCount] = useState(0) const state = useQuery({ queryKey: ['counter', count], queryFn: () => sleep(10).then(() => count), initialData: 99, // ← 触发 DefinedUseQueryResult 分支 }) states.push(state) // ... }

该测试同时验证了一个容易踩坑的行为(测试名为should not show initial data from next query if placeholderData is set):当同时配置initialData: 99placeholderData: keepPreviousData时,切页后展示的是上一页数据(placeholder),而不是下一页的 initialData 99。测试断言中isPlaceholderData会在对应状态为true(见 测试状态断言),这也说明即便data非空,你仍应留意其来源究竟是initialData、真实数据还是placeholderData

六、实际使用方式与触发条件

方式一:直接给 useQuery 传入 initialData

这是最直接、最常见的方式。仓库 DefinedInitialDataOptions 的定义 明确指出:initialData可以是同步返回值的函数,它会被写入 query cache 并持久化,且默认视为 stale(过期)——除非你另行设置了staleTime

import { useQuery } from '@tanstack/preact-query' function Posts() { // 类型上 data: Post[],永远非空 —— 无需判空即可 .map const { data, isError, error } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts, initialData: [], // ← 使返回类型变成 DefinedUseQueryResult<Post[]> }) return ( <div> {isError ? <span>Error: {error.message}</span> : null} <ul>{data.map((post) => <li key={post.id}>{post.title}</li>)}</ul> </div> ) }

注意:这个例子里isError分支只展示错误提示、不中断渲染,其背后正是“refetch 失败时data依然非空”这一类型事实(对应QueryObserverRefetchErrorResult分支)。

方式二:把 initialData 收敛进 queryOptions 统一管理

如果想在useQuery与命令式 API 之间共享“带 initialData”的同一组 options,可以用 queryOptions 把 options 提前声明好,让queryKey携带推断出的数据类型,并在多处复用它:

import { queryOptions, useQuery } from '@tanstack/preact-query' export const postsOptions = queryOptions({ queryKey: ['posts'], queryFn: fetchPosts, initialData: [], // 与上面的 useQuery 重载一致,返回“确定型” }) function Posts() { const { data } = useQuery(postsOptions) // data: Post[],非空 return <ul>{data.map((post) => <li key={post.id}>{post.title}</li>)}</ul> }

这里的类型流向是:queryOptions感知到initialData被设置,从而把自身收窄为DefinedInitialDataOptions,再把该类型透传给useQuery,最终命中返回DefinedUseQueryResult的重载——整条链路在编译期自动完成。

方式三:使用 useSuspenseQuery

采用 Suspense 模式时,组件在数据就绪前不会渲染(由上层 Suspense 边界兜底),因此返回结果天然是确定型的:

import { useSuspenseQuery } from '@tanstack/preact-query' function Post() { // data: Post;且类型上不存在 isPlaceholderData 字段 const { data } = useSuspenseQuery({ queryKey: ['post'], queryFn: fetchPost, }) return <h1>{data.title}</h1> }

触发条件小结

只要满足以下任一情况,查询结果就会以DefinedUseQueryResult(或与其同族的确定型)呈现:

  1. useQuery的 options 中显式包含initialData(值或函数皆可);
  2. 使用useSuspenseQuery(结果为去掉isPlaceholderData的变体);
  3. useQueries中,某一查询项配置了initialData,该项结果的联合成员相应收窄。

七、实战收益与边界注意事项

收益一:删除大量非空断言

未用initialData时,依赖型查询常需要data?.titledata!这类写法。引入DefinedUseQueryResult后,类型系统替你完成了判空,组件渲染分支可显著简化,也降低了运行期undefined访问的风险。

收益二:显式表达“空数据也是合法状态”

对列表类数据使用initialData: [],能在“加载中”与“真正空列表”之间建立清晰区分:首帧即可渲染空列表占位,后台加载完成后无缝替换——这正是类型层面把data收窄为确定值的最典型收益。

注意事项

  • initialData会被持久化并默认视为 stale:只要不设置staleTime,挂载后通常仍会触发一次后台 refetch 以获取最新数据,表现为isFetching: trueisSuccess: true并存。
  • isPlaceholderData只在你使用placeholderData时才有意义:即便类型上是确定型,若同时配置了placeholderData(例如keepPreviousData),isPlaceholderData === true期间展示的并非新请求的数据,禁用“下一页”按钮等交互应以此标志为准,而不是只看data是否为空。
  • 不要对未配置initialDatauseQuery结果做类型断言:该场景返回值本就是UseQueryResultdata可能为undefined是正确建模;若嫌判空繁琐,优先选择补initialData、改用useSuspenseQuery,而不是用as硬断言。
  • TData跟随select变化,而 cache 里的数据类型不变:若你自定义了selectDefinedUseQueryResult<TData>中的TDataselect 之后的类型;需要访问原始缓存数据时,应通过queryClient.getQueryData<TQueryFnData>(...)获取。

八、总结

DefinedUseQueryResult看似只是preact-query类型体系中的一行 re-export,背后却承载着明确的设计意图:让“有 initialData 就有数据”这条运行时事实,上升为可由编译器强制执行的静态保证。从useQuery依据 options 形状选择重载,到 query-core 用DefinedQueryObserverResult联合类型排除pending/loading状态,再到useSuspenseQuery在此基础上剔除恒为falseisPlaceholderData,层层类型收窄最终换来的是更少的空值分支、更直观的组件代码,以及更少被漏掉的undefined边界。建议在 Preact 项目中把“列表先给initialData: []、详情页交给useSuspenseQuery、共享 options 用queryOptions收口”作为默认姿势,即可稳定享受到这套类型体系带来的确定性收益。

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 19:04:06

Android逆向实战:QP棋牌App协议透析与数据流分析

做逆向分析这些年&#xff0c;我其实很少把同一类目标完整走两遍。但最近一个某QP棋牌类App的案例&#xff0c;因为涉及到的协议体系比较典型&#xff0c;我从脱壳到数据流透析又重新手撕了一遍&#xff0c;整个过程踩了不少坑&#xff0c;也沉淀出几条可复用的分析路径。这篇文…

作者头像 李华
网站建设 2026/9/9 19:03:56

AOMTI 2026光电测试技术国际会议:前沿方向与参会指南

1. AOMTI 2026是干什么的&#xff1f;先聊聊会议定位与值得关注的理由 AOMTI 2026&#xff0c;全称先进光电测试技术及仪器国际会议&#xff0c;方向非常聚焦&#xff0c;就是光电测试技术和仪器。这个赛道听起来有点窄&#xff0c;但实际上面特别宽——从激光器出厂前的光束质…

作者头像 李华
网站建设 2026/9/9 19:00:47

如何停止在意他人看法?从神经机制到身份重构的行动清单

你有没有过这样的时刻&#xff1a;写好的东西在发送前反复删改&#xff0c;不是因为写不好&#xff0c;而是怕被人说差&#xff1f;我蹲在屏幕前改标题改了四十分钟&#xff0c;最后发出去的那一版&#xff0c;其实和第一版没什么差别&#xff0c;唯一不同的是&#xff0c;我脑…

作者头像 李华
网站建设 2026/9/9 18:59:36

字节阿里腾讯百度AI岗薪资对比:5年经验为何差50万?

字节、阿里、腾讯、百度的AI岗薪资又被挂出来晒了。这几天好几个朋友转我同样的截图&#xff0c;说有5年经验的算法工程师&#xff0c;总包差距居然拉到50多万&#xff0c;一部分人拿着近两百万的年薪&#xff0c;一部分人还在百万门槛前挣扎。我在这行干了十几年&#xff0c;见…

作者头像 李华