ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Preact Query 的 TypeScript 类型安全实战指南:推断、收窄、error/meta/key 注册与 queryOptions

Preact Query 的 TypeScript 类型安全实战指南:推断、收窄、error/meta/key 注册与 queryOptions Preact Query 的 TypeScript 类型安全实战指南推断、收窄、error/meta/key 注册与 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/queryTanStack Query 的官方类型体系会在数据获取、缓存与状态管理链路中自动传播类型让 Preact 应用在使用tanstack/preact-query时无需手写大量泛型也能获得端到端的类型安全。本篇指南将带你在 Preact Preact Query 项目中系统掌握版本支持策略、useQuery的结果类型推断与基于status的判别联合收窄、error字段的精确化含全局Register注册、meta与查询键的类型注册以及用queryOptions/mutationOptions在 Hook 与命令式 API 之间共享类型化配置、用skipToken类型安全地禁用查询。读完你将具备零显式泛型也能全链路类型安全的工程能力。说明本文对应的官方页面 docs/framework/preact/typescript.md 在文档体系中通过ref: docs/framework/react/typescript.mdreplace: { react-query: preact-query, React: Preact }规则生成因此下文所有代码均为针对tanstack/preact-query的版本相关实现可直接在仓库的 packages/preact-query 中核对。版本支持与类型升级策略Preact QueryTanStack Query 家族在 Preact 上的适配层本身完全用TypeScript编写目的是让库本身和你的项目同时获得类型安全遵循 DefinitelyTyped 的支持窗口support window支持最近两年内发布的 TypeScript 版本在本文撰写时对应TypeScript 5.6 及以上。仓库内类型的变更一律被视为**非破坏性non-breaking**改动通常以patch版本号发布——否则每一次类型增强都会变成一次 major 升级。因此官方强烈建议把依赖锁定到具体的 patch 版本再升级并预期任意两次 release 之间类型可能被修复或增强。与类型无关的公开 API 仍严格遵循 semver 语义化版本。这套策略的落地形态可以在包的入口 packages/preact-query/src/index.ts 中看到preact-query通过export * from tanstack/query-core完整转发核心类型与逻辑再叠加自己的 Hook 层useQuery、useSuspenseQuery、useInfiniteQuery、useMutation、useQueries、useIsMutating等以及queryOptions、mutationOptions、infiniteQueryOptions这些辅助工厂。类型参数默认值、结果类型别名等全部集中在 packages/preact-query/src/types.ts。类型推断让类型沿着数据链路自动流动Preact Query 的类型通常具备极好的穿透性绝大多数场景不需要你手动标注泛型import { useQuery } from tanstack/preact-query const { data } useQuery({ // ^? const data: number | undefined queryKey: [test], queryFn: () Promise.resolve(5), })一旦使用了selectdata的类型会自动变成select的返回类型缓存里仍是完整原始数据只是你看到的data被变换了const { data } useQuery({ // ^? const data: string | undefined queryKey: [test], queryFn: () Promise.resolve(5), select: (data) data.toString(), })从源码看这种推断由UseQueryOptionsTQueryFnData, TError, TData, TQueryKey的泛型链条驱动见 packages/preact-query/src/types.ts#L164-L172TQueryFnData由queryFn返回类型推导TData默认等于TQueryFnData、在传了select时收敛为select的输出类型。给 queryFn 一个明确的返回类型类型穿透效果最好的前提是queryFn具备良定义well-defined的返回类型。注意绝大多数数据请求库默认返回any因此建议把取数逻辑抽成显式返回类型的函数而不是内联裸写const fetchGroups (): PromiseGroup[] axios.get(/groups).then((response) response.data) const { data } useQuery({ queryKey: [groups], queryFn: fetchGroups }) // ^? const data: Group[] | undefined内联调用时useQuery采用对象参数重载而为了应对有initialData数据永不undefined/无initialData两种情况packages/preact-query/src/useQuery.ts 提供了多重重载设置了initialData的重载返回DefinedUseQueryResultstatus为success或error不会出现pending未设置时返回UseQueryResult。这也是为什么同一份参数放不放进initialData会直接影响data是否带| undefined。类型收窄基于 status 的判别联合查询结果使用以status字段为判别条件的判别联合类型discriminated union并派生出对应的布尔标志isPending/isSuccess/isError等。因此可以先检查success/isSuccessTypeScript 就能自动把data收窄为已定义值const { data, isSuccess } useQuery({ queryKey: [test], queryFn: () Promise.resolve(5), }) if (isSuccess) { data // ^? const data: number }这一结果类型的根在tanstack/query-core的QueryObserverResult/DefinedQueryObserverResult上Preact 侧通过 packages/preact-query/src/types.ts#L313-L355 的UseQueryResult、DefinedUseQueryResult直接转发保证 Hook 返回形态与核心观测器一致。给 error 字段精确类型error的默认类型是Error这符合大多数使用者的预期const { error } useQuery({ queryKey: [groups], queryFn: fetchGroups }) // ^? const error: Error如果你确实想抛自定义错误、甚至完全不是Error的东西也可以显式指定 error 的类型const { error } useQueryGroup[], string([groups], fetchGroups) // ^? const error: string | null但代价是一旦你显式传入第一个泛型其余泛型如TData、TQueryKey就不再自动推断。因此官方普遍不推荐抛出非Error的值。如果你抛出的是Error的子类例如AxiosError更好的做法是让error保持默认的Error再借助类型守卫做收窄import axios from axios const { error } useQuery({ queryKey: [groups], queryFn: fetchGroups }) // ^? const error: Error | null if (axios.isAxiosError(error)) { error // ^? const error: AxiosError }注册一个全局 Error 类型TanStack Query v5 提供了无需在调用处写泛型、却能统一全项目error类型的机制——扩充Register接口。这样既能保住类型推断又能让 error 字段变成指定类型如果希望强制每个调用方都做显式收窄把defaultError设为unknownimport tanstack/preact-query declare module tanstack/preact-query { interface Register { // 用 unknown让所有调用处都必须显式收窄。 defaultError: unknown } } const { error } useQuery({ queryKey: [groups], queryFn: fetchGroups }) // ^? const error: unknown | nullRegister接口的真实定义位于核心类型文件 packages/query-core/src/types.ts#L37-L43默认注释给出了各可选槽位defaultError、queryMeta、mutationMeta、queryKey、mutationKey。紧接着它通过条件类型读取Register推断出DefaultErrorpackages/query-core/src/types.ts#L45-L49而DefaultError正是preact-query各选项类型packages/preact-query/src/types.ts中TError DefaultError的默认取值来源。注册全局 Meta 类型与注册全局 error 类似也可以注册全局的Meta类型从而让查询与变更mutation的可选meta字段保持一致并类型安全。注意注册的meta类型必须继承Recordstring, unknown以保证meta仍然是对象import tanstack/preact-query interface MyMeta extends Recordstring, unknown { // 你的 meta 类型定义。 } declare module tanstack/preact-query { interface Register { queryMeta: MyMeta mutationMeta: MyMeta } }这样传入useQuery/queryClient各处选项里的meta都会被统一约束为MyMeta。queryMeta/mutationMeta字段作用范围可分别对照查询与变更的参考文档useQuery、useMutation。注册 queryKey 与 mutationKey 类型同样借助Register你还可以注册全局的QueryKey和MutationKey类型让你的 key 结构匹配应用自身的业务层级并在库的全部 API 表面得到类型约束。注意注册的 key 类型必须继承Array保证 key 仍然是数组import tanstack/preact-query type QueryKey [dashboard | marketing, ...ReadonlyArrayunknown] declare module tanstack/preact-query { interface Register { queryKey: QueryKey mutationKey: QueryKey } }核心侧在解析QueryKey时同时接受Arrayunknown与ReadonlyArrayunknown两种形态否则回退到默认的ReadonlyArrayunknownpackages/query-core/src/types.ts#L51-L59。提取并共享 Query OptionsqueryOptions 工厂把选项内联在useQuery里时能自动推断类型但当你想把查询配置抽成独立函数、同时在useQuery与命令式queryClient.query或prefetch等入口间共享时会丢失推断。这时要用queryOptions把类型找回来import { queryOptions } from tanstack/preact-query function groupOptions() { return queryOptions({ queryKey: [groups], queryFn: fetchGroups, staleTime: 5 * 1000, }) } useQuery(groupOptions()) queryClient.query(groupOptions())更进一步queryOptions返回的queryKey携带了与它关联的queryFn的类型信息我们可以利用这个信息让queryClient.getQueryData也感知到数据类型const data queryClient.getQueryData(groupOptions().queryKey) // ^? const data: Group[] | undefined如果不使用queryOptionsgetQueryData得到的data类型将是unknown除非手动传入泛型const data queryClient.getQueryDataGroup[]([groups])它是怎么做到的查看实现 packages/preact-query/src/queryOptions.tsqueryOptions是个纯恒等函数export function queryOptions(options: unknown) { return options }它的类型重载会为返回的选项补上一个交叉类型QueryKeyWithDataTagTQueryKey, TQueryFnData, TError。这个 DataTag 标记就是核心里getQueryData、queryClient.query等 API 能从queryKey反推出数据与错误类型的依据DataTag 相关的类型基础设施见 packages/query-core/src/types.ts#L61-L84。同时queryOptions.ts的三个重载把参数细分成了三种情况DefinedInitialDataOptions设置了initialDatadata永不undefinedUndefinedInitialDataOptions未设置initialData且允许queryFn: skipTokenUnusedSkipTokenOptions未设置initialDataqueryFn不允许是skipToken。针对每种场景文档注释都解释了真实的行为边界——例如只有enabled: false或定义了默认 query 函数时才能省略queryFn否则仍会发起请求并以Missing queryFn失败initialData并不能阻止这次请求。读懂这些重载你就能精确预判data到底带不带| undefined。getQueriesData 的例外注意queryOptions的类型推断对queryClient.getQueriesData不生效因为它返回的是由异构、unknown数据组成的元组数组。如果你确定这些查询返回的数据类型请显式指定const entries queryClient.getQueriesDataGroup[](groupOptions().queryKey) // ^? const entries: Array[QueryKey, Group[] | undefined]提取 Mutation OptionsmutationOptions 工厂与queryOptions类似mutationOptions可以把变更配置抽成独立函数并在useMutation、useIsMutating、queryClient.isMutating等入口间共享类型function groupMutationOptions() { return mutationOptions({ mutationKey: [addGroup], mutationFn: addGroup, }) } useMutation({ ...groupMutationOptions(), onSuccess: () queryClient.invalidateQueries({ queryKey: [groups] }), }) useIsMutating(groupMutationOptions()) queryClient.isMutating(groupMutationOptions())从 packages/preact-query/src/index.ts#L54 可以看到mutationOptions与queryOptions一样由preact-query导出其类型基础UseMutationOptionsTData, TError, TVariables, TOnMutateResult在 packages/preact-query/src/types.ts#L411-L419最后一个泛型TOnMutateResult专为乐观更新的回滚数据设计——onMutate的返回值会被传递给onSuccess/onError/onSettled。用 skipToken 类型安全地禁用查询如果使用 TypeScript可以用skipToken来禁用查询当你希望基于某个条件暂时禁用查询、又不想破坏类型安全时它是最合适的手段。它是被禁用时的合法queryFn值import { skipToken, useQuery } from tanstack/preact-query function Post({ postId }: { postId: number | undefined }) { const { data, isLoading, isError, error } useQuery({ queryKey: [post, postId], queryFn: postId ! null ? () fetchPost(postId) : skipToken, }) if (postId null) return Select a post if (isLoading) return Loading... if (isError) return spanError: {error.message}/span return h1{data?.title}/h1 }底层实现里skipToken是一个唯一的 Symbol 哨兵packages/query-core/src/utils.ts#L434-L435核心在处理选项时若发现queryFn skipToken会直接跳过执行并在真正被触发时抛出一条明确错误packages/query-core/src/utils.ts#L448-L452。这也是UndefinedInitialDataOptions类型上唯一允许queryFn: skipToken的重载。更完整的禁用策略与enabled的取舍、refetch 行为差异等可参阅 禁用查询指南。实战要点小结能推断就别写泛型useQuery的对象参数写法 类型明确的queryFn已能覆盖绝大多数场景一旦写显式泛型其余泛型的推断就会失效。用判别联合代替可选链先判断status success/isSuccess再访问data即可免去到处?.。error 统一走Error 收窄自定义异常类型用类型守卫收窄全局默认用Register[defaultError]需要强制时设unknown。抽出即注册跨 Hook 与命令式 API 共享的选项一律走queryOptions/mutationOptions数据标签让getQueryData等命令式读取也具备类型。禁止状态用skipToken比enabled: false更贴近类型流尤其适合参数尚未就绪的依赖查询场景。更多延伸阅读可关注官方系列文章中关于类型推断技巧、类型安全最大化以及 Query Options API 设计的内容若想深入源码级验证建议直接阅读 packages/preact-query/src/queryOptions.ts、packages/preact-query/src/types.ts、packages/query-core/src/types.ts 三份类型文件并结合 packages/query-core/src/tests/queryClient.test-d.tsx 中针对skipToken与select的类型级测试用例理解设计意图。【免费下载链接】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),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进