在 tRPC 中推断客户端类型用inferRouterInputs/inferProcedureInput等工具类型打通端到端类型安全【免费下载链接】trpc‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc本指南围绕 www/docs/client/vanilla/infer-types.md 讲解如何在 vanilla原生 TS客户端中复用服务端AppRouter的类型信息无需在客户端重新声明任何类型即可自动拿到路由、procedure 的输入/输出类型以及带类型的TRPCClientError。读完本文你将掌握inferRouterInputs、inferRouterOutputs、inferProcedureInput、inferProcedureOutput、inferSubscriptionInput、inferSubscriptionOutput六个官方推断工具类型的正确用法并理解它们在 tRPC 服务端源码中的实现原理。为什么需要“推断类型”客户端复用服务端AppRoutertRPC 的核心卖点是端到端类型安全服务端用initTRPC构建一个路由树router客户端通过同一份类型定义获得带类型的调用链。为了让客户端也能“按路径”精确访问 API 的类型常见做法是在服务端文件里导出路由的类型// 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((opts) { // imaginary db call return { id: 1, title: tRPC is the best! }; }), create: t .procedure .input(z.object({ title: z.string(), text: z.string() })) .mutation((opts) { // imaginary db call return { id: 1, ...opts.input }; }), onPostAdd: t .procedure .input(z.object({ authorId: z.string() })) .subscription(async function* ({ input }) { // imaginary event source yield { id: 1, title: tRPC is the best!, authorId: input.authorId, }; }), }), }); export type AppRouter typeof appRouter;导出AppRouter之后客户端往往需要“按路径切片”地访问其中某个 procedure 的输入或输出类型——这正是 tRPC 提供推断工具类型的目的。从 packages/server/src/trpc/server/index.ts 可以看到trpc/server统一对外导出以下六个推断工具类型inferRouterInputsTRouter—— 按路由树结构推断所有 procedure 的输入类型inferRouterOutputsTRouter—— 按路由树结构推断所有 procedure 的输出类型inferProcedureInputTProcedure—— 推断单个 procedure 的输入类型inferProcedureOutputTProcedure—— 推断单个 procedure 的输出类型inferSubscriptionInputTProcedure—— 推断单个订阅 procedure 的输入类型inferSubscriptionOutputTProcedure—— 推断单个订阅 procedure 发出的数据yield类型它们都是纯类型层type-level的工具只存在于编译期运行时不产生任何代码因此可以放心地import type引入。推断整棵路由树的输入与输出类型沿用上文AppRouter先推断“整棵路由树按路径组织的输入/输出映射”// client.ts import type { inferRouterInputs, inferRouterOutputs } from trpc/server; import type { AppRouter } from ./server; type RouterInput inferRouterInputsAppRouter; type RouterOutput inferRouterOutputsAppRouter; type PostCreateInput RouterInput[post][create]; // ^? 推断为 { title: string; text: string } type PostCreateOutput RouterOutput[post][create]; // ^? 推断为 { id: number; title: string; text: string }inferRouterInputsAppRouter得到的是一个与AppRouter同构的嵌套对象类型RouterInput[post]对应post子路由RouterInput[post][create]精确命中post.create这条 mutation 的入参结构体。同理inferRouterOutputsAppRouter按相同路径给出各 procedure 的返回类型。也就是说create的输入是{ title: string; text: string }由 zod schema 推导输出则是 resolver 返回的{ id: number; title: string; text: string }。你完全可以把这个嵌套类型交给辅助函数、表单组件或状态管理模块使用避免在客户端重复手写同名结构。从源码实现看inferRouterInputs/inferRouterOutputs定义在 packages/server/src/unstable-core-do-not-import/clientish/inference.ts它们内部借助一个叫GetInferenceHelpers的递归映射类型export type GetInferenceHelpers TType extends input | output, TRoot extends AnyClientTypes, TRecord extends RouterRecord, { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends AnyProcedure ? TType extends input ? inferProcedureInput$Value : inferTransformedProcedureOutputTRoot, $Value : $Value extends RouterRecord ? GetInferenceHelpersTType, TRoot, $Value // 递归进入嵌套子路由 : never : never; };可以看到两条关键规则凡是AnyProcedure的叶子节点按类型求 input/output凡是嵌套的RouterRecord就递归展开因此无论路由树嵌套多深RouterInput/RouterOutput都保持与AppRouter相同的形状。推断单个 procedure 的输入与输出类型如果你手上已经有某个具体 procedure例如通过AppRouter[post][byId]取到了它可以直接用inferProcedureInput/inferProcedureOutput精确推断而不必维护整棵 Router 类型的中间变量// client.ts import type { inferProcedureInput, inferProcedureOutput } from trpc/server; import type { AppRouter } from ./server; type PostByIdInput inferProcedureInputAppRouter[post][byId]; // ^? 推断为 stringbyId 的输入是 z.string() type PostByIdOutput inferProcedureOutputAppRouter[post][byId]; // ^? 推断为 { id: number; title: string }这两个工具类型的实现位于 packages/server/src/unstable-core-do-not-import/procedure.ts。它们的核心是提取 procedure 的_def[$types]即 procedure 定义中保存的类型元数据export type inferProcedureParamsTProcedure TProcedure extends AnyProcedure ? TProcedure[_def] : never; export type inferProcedureOutputTProcedure inferProcedureParamsTProcedure[$types][output]; export type inferProcedureInputTProcedure extends AnyProcedure undefined extends inferProcedureParamsTProcedure[$types][input] ? void | inferProcedureParamsTProcedure[$types][input] : inferProcedureParamsTProcedure[$types][input];这段源码还揭示了一个容易被忽略的语义细节当 procedure 没有声明.input()时它的输入类型被视为undefined推断结果会并入void。例如上文的post.list没有入参则inferProcedureInputAppRouter[post][list]会得到void这与调用端post.list.query()不传参的使用方式一致。值得注意的是inferRouterInputs/inferProcedureInput只处理运行时经校验后的输入服务端对输入做过的 validator 逻辑如.refine等不会体现在类型上这是推断类型的边界所在。订阅subscription的输入与输出类型推断tRPC 的订阅与普通 query/mutation 不同procedure 的 resolver 不是返回单个值而是通过async function*持续yield数据。因此 tRPC 为订阅提供了专用的一对工具类型// client.ts import type { inferSubscriptionInput, inferSubscriptionOutput, } from trpc/server; import type { AppRouter } from ./server; type OnPostAddInput inferSubscriptionInputAppRouter[post][onPostAdd]; // ^? 推断为 { authorId: string }订阅建立时携带的入参 type OnPostAddOutput inferSubscriptionOutputAppRouter[post][onPostAdd]; // ^? 推断为 { id: number; title: string; authorId: string }每次 yield 的数据inferSubscriptionInputTProcedure本质复用inferProcedureInput订阅建立时的入参而inferSubscriptionOutputTProcedure则针对新版异步生成器式订阅async function*额外做了一层解包取 async iterable 每次yield的类型而非整个生成器的类型。见 packages/server/src/unstable-core-do-not-import/procedure.tsexport type inferSubscriptionInputTProcedure extends AnySubscriptionProcedure inferProcedureInputTProcedure; export type inferSubscriptionOutputTProcedure extends AnySubscriptionProcedure TProcedure extends LegacyObservableSubscriptionProcedureany ? inferProcedureOutputTProcedure : inferAsyncIterableYieldinferProcedureOutputTProcedure;inferSubscriptionOutput内部用inferAsyncIterableYield把AsyncGenerator的产出值解出来——这正是订阅客户端事件回调里拿到的“每条数据”的类型。如果你把inferProcedureOutput用在订阅 procedure 上得到的将是不含解包的生成器类型因此订阅请务必使用inferSubscriptionOutput。对于订阅的实践细节如何在客户端建立连接、监听数据与关闭可继续阅读 vanilla 客户端文档 aborting-procedures。推断带路由形状的TRPCClientError错误类型除数据本身外客户端错误处理也需要类型。TRPCClientError可以携带泛型router 或单个 procedure使cause.data具备服务端错误结构中定义的类型。tRPC 客户端还在 packages/client/src/TRPCClientError.ts 中提供了配套的isTRPCClientError类型守卫用于把unknown收窄为TRPCClientError// trpc.ts —— 先建立带类型的客户端 import { createTRPCClient, httpBatchLink } from trpc/client; import type { AppRouter } from ./server; export const trpc createTRPCClientAppRouter({ links: [ httpBatchLink({ url: http://localhost:3000/api/trpc, }), ], });// client.ts import { TRPCClientError } from trpc/client; import type { AppRouter } from ./server; import { trpc } from ./trpc; export function isTRPCClientError( cause: unknown, ): cause is TRPCClientErrorAppRouter { return cause instanceof TRPCClientError; } async function main() { try { await trpc.post.byId.query(1); } catch (cause) { if (isTRPCClientError(cause)) { // cause 现在被收窄为 AppRouter 对应的 TRPCClientError console.log(data, cause.data); // data 中的字段如 httpStatus、code、path均带类型提示 } else { // 非 tRPC 错误网络错误、非 JSON 响应等走这里 } } } main();要点拆解createTRPCClientAppRouter(...)让trpc上每个方法调用都带上类型错误对象在抛出时仍是unknown因此需要isTRPCClientError守卫收窄。cause instanceof TRPCClientError是运行时检查返回的cause is TRPCClientErrorAppRouter谓词让 TypeScript 在if分支内自动获得强类型。TRPCClientErrorAppRouter内部通过inferErrorShape结合服务端配置推导cause.data的具体结构code、httpStatus、path等见 TRPCClientError.ts 对错误形状泛型的约束。若把泛型参数换成单个 procedure还能让错误对象与特定调用点对齐。源码佐证这些工具类型从哪里来、如何被引用导出入口六个推断类型统一从trpc/server导出packages/server/src/trpc/server/index.ts。旧版本中inferProcedureInput/inferProcedureOutput曾从trpc/server/shared或trpc/server的子路径导出现在这些位置已被标记deprecated见 packages/server/src/shared.ts一律改为从trpc/server直接导入。类型语义AnyProcedure覆盖 query/mutation/subscriptionprocedure.ts因此inferProcedureInput/inferProcedureOutput对三种 procedure 通用订阅另有专属包装。序列化边角inferRouterOutputs走的是inferTransformedProcedureOutputinference.ts当未启用数据转换器transformer为false默认情况时会对输出做一次Serialize处理模拟数据经过 JSON 序列化传输后的形态该内部类型也随trpc/server导出但属于低层 API日常使用推荐上面六个官方类型。服务端调用复用同样的推断体系也被用于服务端直调server-side call场景相关示例可参考 server-side-calls.mdReact 版客户端存在完全同构的一篇文档 www/docs/client/react/infer-types.md若你在 React 中做 RSC/SSG 数据预取可对照阅读。小结与实践建议推断类型全部来自服务端export type AppRouter typeof appRouter客户端零重复定义即可获得路由树级或单 procedure 级的类型信息新增、删除路由或修改入参后客户端类型自动同步这就是 tRPC“类型即契约”的落地方式。需要“按路径取一段”时用inferRouterInputsAppRouter/inferRouterOutputsAppRouter支持任意深度的嵌套子路由已持有具体 procedure 引用时用inferProcedureInput/inferProcedureOutput订阅 procedure 请使用inferSubscriptionInput/inferSubscriptionOutput后者会自动解包 async generator 的 yield 类型。错误处理用TRPCClientErrorAppRouterisTRPCClientError守卫即可获得随服务端错误形状同步的强类型cause.data。这些类型均为编译期结构运行时不产生额外开销官方文档的完整示例即当前仓库中的 infer-types.mdvanilla 客户端的初始化可参考 setup.mdx整体使用模式见 overview.md。【免费下载链接】trpc‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考