news 2026/9/9 23:21:25

tRPC React Query 类型推断完全指南:从 `inferRouterInputs` 到 `RouterLike` / `UtilsLike`(v10 版)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
tRPC React Query 类型推断完全指南:从 `inferRouterInputs` 到 `RouterLike` / `UtilsLike`(v10 版)

tRPC React Query 类型推断完全指南:从inferRouterInputsRouterLike/UtilsLike(v10 版)

【免费下载链接】trpc🧙‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc

本指南以 www/versioned_docs/version-10.x/client/react/infer-types.md 为骨架,结合@trpc/react-query的源码实现与测试用例展开,面向 tRPC v10 + React Query 的开发者。你将掌握两件事:一是如何用inferReactQueryProcedureOptions让自定义 Hook 的 options 参数与路由全链路类型对齐;二是如何借助@trpc/react-query/shared导出的RouterLike/UtilsLike抽象类型,把“router 工厂”生成的同构路由下发给多个前端组件复用,彻底告别手工维护类型。


1. 先厘清:类型推断的两层能力

tRPC 的类型安全来自一套“一次定义、处处推导”的机制。服务端用t.router({...})定义好appRouter,并导出export type AppRouter = typeof appRouter,客户端所有 hook、调用、错误处理都围绕这一单一类型来源展开。

在 React 集成里,类型推断其实分为两层:

  1. 路由层(跨客户端通用)@trpc/server导出的inferRouterInputs<TRouter>inferRouterOutputs<TRouter>,可把AppRouter中各 procedure 的入参(input)与出参(output)映射成一个可索引的深层对象类型。这部分在 www/versioned_docs/version-10.x/client/vanilla/infer-types.md 中有完整讲解,底层实现位于 packages/server/src/unstable-core-do-not-import/clientish/inference.ts。
  2. React Query 层(本主题)@trpc/react-query集成在路由层之上再提供专属的推断辅助类型,专门服务useQuery/useMutation/useUtils等 React 场景——这正是本文的核心。

两者的分工可以概括为:inferRouterInputs回答“过程需要什么输入、返回什么输出”,而inferReactQueryProcedureOptions回答“某个 procedure 的 React Query options(如enabledretryonSuccess)长得什么样”。

版本说明:本文对应 tRPC v10 文档目录。v10 的 React 集成包名为@trpc/react-query,对应本仓库 packages/react-query;仓库中同时存在的packages/tanstack-react-query是 v11 面向新版 TanStack React Query 的集成,二者概念相通但导入路径不同。

2. 贯穿全文的示例 Router

文档中的示例以一个post子路由为演示对象,它包含三种典型 procedure:无输入 query、带z.string()输入 query、带对象输入 mutation。

// server.ts import { initTRPC } from '@trpc/server'; import { z } from "zod"; const t = initTRPC.create(); const appRouter = t.router({ post: t.router({ list: t.procedure .query(() => { // imaginary db call return [{ id: 1, title: 'tRPC is the best!' }]; }), byId: t.procedure .input(z.string()) .query(({ input }) => { // imaginary db call return { id: 1, title: 'tRPC is the best!' }; }), create: t.procedure .input(z.object({ title: z.string(), text: z.string(), })) .mutation(({ input }) => { // imaginary db call return { id: 1, ...input }; }), }), }); export type AppRouter = typeof appRouter;

这里list无输入、byIdz.string()为输入、create{ title, text }为输入。后续所有类型推断都建立在这个AppRouter上。

3.inferReactQueryProcedureOptions:让自定义 Hook 的 options 也拥有类型

3.1 使用动机

在创建围绕 tRPC procedure 的自定义 Hook(如把usePostCreateusePostById封装成业务层组件)时,通常需要透传useQuery/useMutation的配置。如果手写类型,只要服务端入参或返回结构一变,前端就会悄悄产生类型漂移。正确做法是直接从 router 推导。

@trpc/react-query为此导出了inferReactQueryProcedureOptions辅助类型。先在一个集中式trpc.ts文件里一次性声明:

// trpc.ts import { createTRPCReact, type inferReactQueryProcedureOptions, } from '@trpc/react-query'; import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'; import type { AppRouter } from './server'; // infer the types for your router export type ReactQueryOptions = inferReactQueryProcedureOptions<AppRouter>; export type RouterInputs = inferRouterInputs<AppRouter>; export type RouterOutputs = inferRouterOutputs<AppRouter>; export const trpc = createTRPCReact<AppRouter>();
  • ReactQueryOptions:按“过程路径”组织的 React Query options 类型;
  • RouterInputs/RouterOutputs:来自@trpc/server的通用推断,供需要手写输入/输出类型的场景使用;
  • trpc:类型化的 React hooks 客户端(createTRPCReact<AppRouter>(),可参考 www/versioned_docs/version-10.x/client/react/setup.mdx)。

3.2 封装带 options 透传的 mutation Hook

以“创建 post 后失效整个 post 路由缓存”为例。usePostCreate接收可选的PostCreateOptions(即ReactQueryOptions['post']['create']),内部用展开运算符透传所有用户 options,再叠加一段固定逻辑——在onSuccess里调用utils.post.invalidate()使post路由下的所有查询失效,同时仍然调用用户自己的onSuccess

// usePostCreate.ts import { trpc, type ReactQueryOptions, type RouterInputs, type RouterOutputs, } from './trpc'; type PostCreateOptions = ReactQueryOptions['post']['create']; function usePostCreate(options?: PostCreateOptions) { const utils = trpc.useUtils(); return trpc.post.create.useMutation({ ...options, onSuccess(post) { // invalidate all queries on the post router // when a new post is created utils.post.invalidate(); options?.onSuccess?.(post); }, }); }

关键点在于:...options展开后,Hook 调用方传入的enabledretryonError等配置依然会被保留;而onSuccess被重写为“先失效、再回调用户逻辑”的组合,且post参数的类型自动是post.create的输出类型。

3.3 封装 query Hook 并分别取 input / options 类型

对 query 场景,可把输入与 options 分开索引。PostByIdInput取自RouterInputs['post']['byId'](即string),PostByIdOptions取自ReactQueryOptions['post']['byId']

// usePostById.ts import { ReactQueryOptions, RouterInputs, trpc } from './trpc'; type PostByIdOptions = ReactQueryOptions['post']['byId']; type PostByIdInput = RouterInputs['post']['byId']; function usePostById(input: PostByIdInput, options?: PostByIdOptions) { return trpc.post.byId.useQuery(input, options); }

封装完成后,Hook 的调用方无需useQuery第一手接触trpc代理,却仍能获得完整的服务端校验提示:input传非字符串会直接报错,返回数据也会被推断为{ id, title }

3.4 源码深挖:这个 helper 到底做了什么

从源码看,inferReactQueryProcedureOptions是一个“沿 router 记录逐层映射”的类型级递归,定义于 packages/react-query/src/utils/inferReactQueryProcedure.ts,并经由 packages/react-query/src/index.ts 对外导出:

type inferReactQueryProcedureOptionsInner< TRoot extends AnyRootTypes, TRecord extends RouterRecord, > = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends AnyQueryProcedure ? InferQueryOptions<TRoot, $Value> : $Value extends AnyMutationProcedure ? InferMutationOptions<TRoot, $Value> : $Value extends RouterRecord ? inferReactQueryProcedureOptionsInner<TRoot, $Value> : never : never; }; export type inferReactQueryProcedureOptions<TRouter extends AnyRouter> = inferReactQueryProcedureOptionsInner< TRouter['_def']['_config']['$types'], TRouter['_def']['record'] >;

可以拆解出三层逻辑:

  1. 递归结构保持:对RouterRecord逐 key 映射。若当前值仍是一个嵌套路由(RouterRecord),就递归调用自身——这解释了为什么ReactQueryOptions['post']['create']能按post.create的路径索引到深层 options。
  2. 过程分类:若当前值命中AnyQueryProcedure,映射为InferQueryOptions;命中AnyMutationProcedure则映射为InferMutationOptions;两者皆非的普通值收敛为never,防止脏类型进入结果。
  3. 选项收窄:同文件的InferQueryOptions通过Omit<..., 'select' | 'queryFn'>UseTRPCQueryOptions中剥离了selectqueryFn,因为这些字段本就不应暴露给调用方;数据默认类型取自inferTransformedProcedureOutput(即经过 data transformer 处理后的输出类型),错误类型统一绑定为TRPCClientErrorLike<TRoot>

换言之,ReactQueryOptions['post']['byId']trpc.post.byId.useQuery内部实际接受的 options 类型同源,这正是“自定义 Hook 与内置 Hook 类型永不漂移”的保障。同样,useMutation.test.tsx等测试也直接消费了该 helper 以校验配置类型:packages/react-query/test/useMutation.test.tsx。

4. Router Factory 与多态:为“工厂函数”生成抽象类型

4.1 适用场景与动机

当应用中存在“多次创建结构相似的路由”的工厂函数(典型如:共享的 CSV 导出系统要挂到多个实体上、一个通用 CRUD 路由要适配多个数据源)时,很自然地希望不同实例之间共享同一套前端组件代码

问题在于:@trpc/react-query生成的 router 代理接口是高度具体的——两棵结构完全相同的路由,在 TypeScript 的结构化类型系统下并不总是被视为互相兼容(可参考 packages/react-query/test/polymorphism.test.tsx 开头的注释说明)。于是@trpc/react-query/shared导出若干**Like抽象类型,用来生成“与某一路由接口形状兼容”的抽象视图,从而可以把 router 作为 prop 传给通用 React 组件。

4.2 第一步:定义工厂并导出抽象类型

下面的createMyRouter()由你自己实现,唯一要求是随后能从它的返回值推导出t.router()的类型:

// api/factory.ts import { t, publicProcedure } from './trpc'; // @trpc/react-query/shared exports several **Like types which can be used to generate abstract types import { RouterLike, UtilsLike } from '@trpc/react-query/shared'; // Factory function written by you, however you need, // so long as you can infer the resulting type of t.router() later export function createMyRouter() { return t.router({ createThing: publicProcedure .input(ThingRequest) .output(Thing) .mutation(/* do work */), listThings: publicProcedure .input(ThingQuery) .output(ThingArray) .query(/* do work */), }) } // Infer the type of your router, and then generate the abstract types for use in the client type MyRouterType = ReturnType<typeof createMyRouter> export MyRouterLike = RouterLike<MyRouterType> export MyRouterUtilsLike = UtilsLike<MyRouterType>

文档原始片段中ThingRequest/Thing/ThingArray/ThingQuery为省略的 zod 结构,export MyRouterLike亦应补上type关键字;一份可运行、已定义好 zod schema(ThingRequest = z.object({ name })Thing = z.object({ id, name })ThingQuery = z.object({ filter })等)的完整实现可参考 www/docs/client/react/infer-types.md(v11 文档已修订该示例)。

三个导出的要点:

  • MyRouterLike:描述“与工厂返回路由形状兼容”的调用侧代理类型(query/mutation 调用方式),对应源码 packages/react-query/src/shared/polymorphism/routerLike.ts;
  • MyRouterUtilsLike:描述对应的utils/上下文路径(如invalidatesetData等),对应源码 packages/react-query/src/shared/polymorphism/utilsLike.ts;
  • 二者连同QueryLike/MutationLike由 packages/react-query/src/shared/polymorphism/index.ts 统一导出,因此客户端可从@trpc/react-query/shared子路径导入。

4.3 第二步:把抽象类型随AppRouter一起导出

后端入口再把这些抽象类型转发给客户端:

// api/server.ts export type AppRouter = typeof appRouter; // Export your MyRouter types to the client export type { MyRouterLike, MyRouterUtilsLike } from './factory';

这一步意味着:客户端永远不直接依赖工厂内部的实现细节,只依赖公开的抽象类型契约

4.4 第三步:编写接收routeutils的通用组件

前端组件以 props 接收符合抽象契约的routeutils,内部就能像使用具体路由一样调用useQuery/useMutation,并获得完整的输入输出校验:

// frontend/usePostCreate.ts import type { MyRouterLike, MyRouterUtilsLike, trpc, useUtils } from './trpc'; type MyGenericComponentProps = { route: MyRouterLike; utils: MyRouterUtilsLike; }; function MyGenericComponent(props: MyGenericComponentProps) { const { route } = props; const thing = route.listThings.useQuery({ filter: 'qwerty', }); const mutation = route.doThing.useMutation({ onSuccess() { props.utils.listThings.invalidate(); }, }); function handleClick() { mutation.mutate({ name: 'Thing 1', }); } return; /* ui */ } function MyPageComponent() { const utils = useUtils(); return ( <MyGenericComponent route={trpc.deep.route.things} utils={utils.deep.route.things} /> ); } function MyOtherPageComponent() { const utils = useUtils(); return ( <MyGenericComponent route={trpc.different.things} utils={utils.different.things} /> ); }

这是本模式的核心收益:同一个<MyGenericComponent />,只需传入不同深度/不同命名空间下的同构路由路径trpc.deep.route.thingstrpc.different.things)即可复用,组件无需关心数据到底挂在哪个业务命名空间下。

值得注意的细节:在源码示例中,一个真实的通用导出组件常常同时具备start(mutation)、list(query)、status(query)等一组“同构过程”,而工厂通过闭包注入不同的dataProvider,实现对 issues、discussions、pull requests 等异构数据源共用同一套路由实现——见 packages/react-query/test/polymorphism.factory.tsx 中createExportRoute(baseProcedure, dataProvider)的写法。

4.5 源码深挖:RouterLikeUtilsLike的实现与测试佐证

RouterLike的内部实现与inferReactQueryProcedureOptions高度同构——同样沿RouterRecord递归,遇到 query 过程产出QueryLike、mutation 过程产出MutationLike、嵌套路由继续递归(packages/react-query/src/shared/polymorphism/routerLike.ts):

export type RouterLikeInner< TRoot extends AnyRootTypes, TRecord extends RouterRecord, > = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends AnyQueryProcedure ? QueryLike<TRoot, $Value> : $Value extends AnyMutationProcedure ? MutationLike<TRoot, $Value> : $Value extends RouterRecord ? RouterLikeInner<TRoot, $Value> : never : never; };

UtilsLike直接复用装饰后的 utils 代理记录(packages/react-query/src/shared/polymorphism/utilsLike.ts):

export type UtilsLike<TRouter extends AnyRouter> = DecoratedProcedureUtilsRecord< TRouter['_def']['_config']['$types'], TRouter['_def']['record'] >;

QueryLike/MutationLike还额外导出了InferQueryLikeInput/InferQueryLikeData/InferMutationLikeInput/InferMutationLikeData等辅助推断类型(见 packages/react-query/src/shared/polymorphism/queryLike.ts、packages/react-query/src/shared/polymorphism/mutationLike.ts),可用来从抽象类型反推输入/数据形状。

这套“多态”(polymorphism)机制在仓库中有完整的端到端测试佐证:

  • packages/react-query/test/polymorphism.test.tsx:构造了github.issuesgithub.discussionsgithub.pullRequests等多个共享“文件导出”接口的路由,并验证通用组件可被这些不同命名空间下的路由实例化使用;
  • packages/react-query/test/polymorphism.factory.tsx:定义createExportRoute<TBaseProcedure>(baseProcedure, dataProvider)工厂与导出的抽象类型;
  • packages/react-query/test/polymorphism.common.tsx 与 packages/react-query/test/polymorphism.subtyped-factory.tsx:分别提供共享根类型配置与“子类型化工厂”(在基础工厂之上扩展实体子类型与额外 procedure)。

v10 文档末尾指向的完整示例(原文档给出的是 GitHub 外链,此处对应仓库内路径)即为上述 packages/react-query/test/polymorphism.test.tsx 及其伴随工厂文件。

5. 常见问题与最佳实践小结

  1. 两个 helper 不要混淆inferReactQueryProcedureOptions处理的是“React Query hook 的 options 类型”,面向自定义 Hook 封装;inferRouterInputs/inferRouterOutputs处理的是“procedure 的输入/输出数据形状”,跨框架通用。二者通常成对导出(如trpc.ts中同时导出ReactQueryOptionsRouterInputsRouterOutputs)。
  2. 统一收敛在一个trpc.ts:把类型别名与trpc客户端放在同一模块再分发,能避免各组件散落createTRPCReact重复实例化。
  3. 失效缓存配合抽象类型RouterLike只管“怎么调”,UtilsLike管“怎么失效/写缓存”,通用组件应同时以 props 接收两者,如示例中props.utils.listThings.invalidate()的调用。
  4. 工厂命名空间差异化:只要工厂产出的路由结构同构,无论它嵌套多深(trpc.deep.route.things),也无论挂在几个命名空间下(different.things),抽象类型都能让组件保持单一实现——这是将“复用”从数据层提升到类型层的核心手段。
  5. 以测试为参照:想落地生产级多态路由,直接阅读 packages/react-query/test/polymorphism.test.tsx 与 packages/react-query/test/polymorphism.factory.tsx 比照示例,是最快的上手路径。

6. 延伸阅读

  • 路由层通用推断(inferRouterInputs/inferRouterOutputs/TRPCClientError):www/versioned_docs/version-10.x/client/vanilla/infer-types.md
  • React Query 集成安装与初始化:<www/versioned_docs/version-10.x/client/react/setup.mdx>
  • useUtilsinvalidaterefetchsetData等)API:<www/versioned_docs/version-10.x/client/react/useUtils.mdx>
  • 相关 hooks 文档:useQueryuseMutationuseQueriesuseInfiniteQuerysuspense,见 www/versioned_docs/version-10.x/client/react 目录
  • 核心实现文件:packages/react-query/src/utils/inferReactQueryProcedure.ts、packages/react-query/src/shared/polymorphism/routerLike.ts、packages/react-query/src/shared/polymorphism/utilsLike.ts

【免费下载链接】trpc🧙‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc

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

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

TVBoxOSC电视盒子播放器完整配置教程:5步装好跑起来

TVBoxOSC电视盒子播放器完整配置教程&#xff1a;5步装好跑起来 【免费下载链接】TVBoxOSC TVBoxOSC - 一个基于第三方项目的代码库&#xff0c;用于电视盒子的控制和管理。 项目地址: https://gitcode.com/GitHub_Trending/tv/TVBoxOSC TVBoxOSC是面向电视盒子与智能电…

作者头像 李华
网站建设 2026/9/9 23:16:44

推客系统化运营:从人工记账到数据驱动的团队管理

做了这么多年推客&#xff0c;我发现一个特别扎心的现象&#xff1a;同样是从零开始的一批人&#xff0c;有人每天忙到半夜&#xff0c;朋友圈刷屏、群里发广告、挨个私聊&#xff0c;月底一看佣金还是那三五千&#xff1b;另一拨人看着也没多拼命&#xff0c;但单子就是不停出…

作者头像 李华
网站建设 2026/9/9 23:12:27

大数据架构选型指南:从批处理到实时计算的关键决策

1. 为什么"先选型、后开发"在大数据架构里是生死问题我见过太多团队把大数据项目做砸&#xff0c;原因几乎都不是写代码的能力不行&#xff0c;而是从一开始就把架构选型这件事当成了"技术调研报告"来应付。开会时大家对着几张对比表格点头&#xff0c;最后…

作者头像 李华
网站建设 2026/9/9 23:11:37

基于Java的大学生创新成果信息管理系统设计与实现

做毕业设计选题目&#xff0c;最怕的就是“大而空”或者“旧而泛”。如果你拿到了“基于Java的大学生创新成果信息管理系统”这个题&#xff0c;或者正在这个方向里选型&#xff0c;我想先给你吃颗定心丸&#xff1a;这是一个非常典型的、能拿高分、也能锻炼完整Java后端能力的…

作者头像 李华