news 2026/9/10 14:22:23

在 tRPC 中推断客户端类型:用 `inferRouterInputs`/`inferProcedureInput` 等工具类型打通端到端类型安全

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
在 tRPC 中推断客户端类型:用 `inferRouterInputs`/`inferProcedureInput` 等工具类型打通端到端类型安全

在 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。读完本文你将掌握inferRouterInputsinferRouterOutputsinferProcedureInputinferProcedureOutputinferSubscriptionInputinferSubscriptionOutput六个官方推断工具类型的正确用法,并理解它们在 tRPC 服务端源码中的实现原理。

为什么需要“推断类型”:客户端复用服务端AppRouter

tRPC 的核心卖点是端到端类型安全:服务端用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统一对外导出以下六个推断工具类型:

  • inferRouterInputs<TRouter>—— 按路由树结构推断所有 procedure 的输入类型
  • inferRouterOutputs<TRouter>—— 按路由树结构推断所有 procedure 的输出类型
  • inferProcedureInput<TProcedure>—— 推断单个 procedure 的输入类型
  • inferProcedureOutput<TProcedure>—— 推断单个 procedure 的输出类型
  • inferSubscriptionInput<TProcedure>—— 推断单个订阅 procedure 的输入类型
  • inferSubscriptionOutput<TProcedure>—— 推断单个订阅 procedure 发出的数据(yield)类型

它们都是纯类型层(type-level)的工具,只存在于编译期,运行时不产生任何代码,因此可以放心地import type引入。

推断整棵路由树的输入与输出类型

沿用上文AppRouter,先推断“整棵路由树按路径组织的输入/输出映射”:

// client.ts import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'; import type { AppRouter } from './server'; type RouterInput = inferRouterInputs<AppRouter>; type RouterOutput = inferRouterOutputs<AppRouter>; type PostCreateInput = RouterInput['post']['create']; // ^? 推断为 { title: string; text: string } type PostCreateOutput = RouterOutput['post']['create']; // ^? 推断为 { id: number; title: string; text: string }
  • inferRouterInputs<AppRouter>得到的是一个与AppRouter同构的嵌套对象类型RouterInput['post']对应post子路由,RouterInput['post']['create']精确命中post.create这条 mutation 的入参结构体。
  • 同理,inferRouterOutputs<AppRouter>按相同路径给出各 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> : inferTransformedProcedureOutput<TRoot, $Value> : $Value extends RouterRecord ? GetInferenceHelpers<TType, 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 = inferProcedureInput<AppRouter['post']['byId']>; // ^? 推断为 string(byId 的输入是 z.string()) type PostByIdOutput = inferProcedureOutput<AppRouter['post']['byId']>; // ^? 推断为 { id: number; title: string }

这两个工具类型的实现位于 packages/server/src/unstable-core-do-not-import/procedure.ts。它们的核心是提取 procedure 的_def['$types'](即 procedure 定义中保存的类型元数据):

export type inferProcedureParams<TProcedure> = TProcedure extends AnyProcedure ? TProcedure['_def'] : never; export type inferProcedureOutput<TProcedure> = inferProcedureParams<TProcedure>['$types']['output']; export type inferProcedureInput<TProcedure extends AnyProcedure> = undefined extends inferProcedureParams<TProcedure>['$types']['input'] ? void | inferProcedureParams<TProcedure>['$types']['input'] : inferProcedureParams<TProcedure>['$types']['input'];

这段源码还揭示了一个容易被忽略的语义细节:当 procedure 没有声明.input()时,它的输入类型被视为undefined,推断结果会并入void。例如上文的post.list没有入参,则inferProcedureInput<AppRouter['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 = inferSubscriptionInput<AppRouter['post']['onPostAdd']>; // ^? 推断为 { authorId: string }(订阅建立时携带的入参) type OnPostAddOutput = inferSubscriptionOutput<AppRouter['post']['onPostAdd']>; // ^? 推断为 { id: number; title: string; authorId: string }(每次 yield 的数据)

inferSubscriptionInput<TProcedure>本质复用inferProcedureInput(订阅建立时的入参),而inferSubscriptionOutput<TProcedure>则针对新版异步生成器式订阅(async function*)额外做了一层解包:取 async iterable 每次yield的类型,而非整个生成器的类型。见 packages/server/src/unstable-core-do-not-import/procedure.ts:

export type inferSubscriptionInput<TProcedure extends AnySubscriptionProcedure> = inferProcedureInput<TProcedure>; export type inferSubscriptionOutput<TProcedure extends AnySubscriptionProcedure> = TProcedure extends LegacyObservableSubscriptionProcedure<any> ? inferProcedureOutput<TProcedure> : inferAsyncIterableYield<inferProcedureOutput<TProcedure>>;

inferSubscriptionOutput内部用inferAsyncIterableYieldAsyncGenerator的产出值解出来——这正是订阅客户端事件回调里拿到的“每条数据”的类型。如果你把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 = createTRPCClient<AppRouter>({ 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 TRPCClientError<AppRouter> { 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();

要点拆解:

  • createTRPCClient<AppRouter>(...)trpc上每个方法调用都带上类型;错误对象在抛出时仍是unknown,因此需要isTRPCClientError守卫收窄。
  • cause instanceof TRPCClientError是运行时检查,返回的cause is TRPCClientError<AppRouter>谓词让 TypeScript 在if分支内自动获得强类型。
  • TRPCClientError<AppRouter>内部通过inferErrorShape结合服务端配置推导cause.data的具体结构(codehttpStatuspath等),见 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/subscription(procedure.ts),因此inferProcedureInput/inferProcedureOutput对三种 procedure 通用,订阅另有专属包装。
  • 序列化边角inferRouterOutputs走的是inferTransformedProcedureOutput(inference.ts):当未启用数据转换器(transformerfalse,默认情况)时会对输出做一次Serialize处理,模拟数据经过 JSON 序列化传输后的形态;该内部类型也随@trpc/server导出但属于低层 API,日常使用推荐上面六个官方类型。
  • 服务端调用复用:同样的推断体系也被用于服务端直调(server-side call)场景,相关示例可参考 server-side-calls.md;React 版客户端存在完全同构的一篇文档 www/docs/client/react/infer-types.md,若你在 React 中做 RSC/SSG 数据预取,可对照阅读。

小结与实践建议

  • 推断类型全部来自服务端export type AppRouter = typeof appRouter,客户端零重复定义即可获得路由树级或单 procedure 级的类型信息;新增、删除路由或修改入参后,客户端类型自动同步,这就是 tRPC“类型即契约”的落地方式。
  • 需要“按路径取一段”时用inferRouterInputs<AppRouter>/inferRouterOutputs<AppRouter>(支持任意深度的嵌套子路由);已持有具体 procedure 引用时用inferProcedureInput/inferProcedureOutput;订阅 procedure 请使用inferSubscriptionInput/inferSubscriptionOutput,后者会自动解包 async generator 的 yield 类型。
  • 错误处理用TRPCClientError<AppRouter>+isTRPCClientError守卫,即可获得随服务端错误形状同步的强类型cause.data
  • 这些类型均为编译期结构,运行时不产生额外开销;官方文档的完整示例即当前仓库中的 infer-types.md,vanilla 客户端的初始化可参考 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),仅供参考

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

LCoS技术驱动AR-HUD革新:问界量产方案解析

1. AR-HUD技术爆发&#xff1a;从问界看LCoS方案的量产突围最近汽车圈里有个数字特别亮眼——AR-HUD月交付量突破20万辆大关。作为车载显示领域的"顶流"配置&#xff0c;这个数字背后藏着两个关键角色&#xff1a;问界系列车型和LCoS显示技术。我拆解过十几款AR-HUD方…

作者头像 李华
网站建设 2026/9/10 14:19:31

机械毕业设计之瓜果切丝切片机的结构设计

题目&#xff1a;机械毕业设计之瓜果切丝切片机的结构设计一、项目介绍随着食品工业的快速发展&#xff0c;对高效、稳定的瓜果切丝切片机需求日益增长。本文聚焦于瓜果切丝切片机的结构设计与性能优化研究&#xff0c;旨在解决现有设备在切割效率、质量稳定性及能耗等方面的不…

作者头像 李华
网站建设 2026/9/10 14:19:19

Vue AI 技术架构与智能开发实践

1. Vue AI 技术架构解析Vue AI作为Vue.js生态中的新一代智能开发工具&#xff0c;其核心架构采用了模块化设计理念。底层基于Vue 3的Composition API构建&#xff0c;通过智能代码生成引擎和上下文感知系统实现开发辅助功能。1.1 核心功能模块智能代码补全系统&#xff1a;采用…

作者头像 李华
网站建设 2026/9/10 14:19:16

Linux内存管理:页表与TLB原理及优化实践

1. Linux内存管理核心机制解析在Linux系统中&#xff0c;Page Table&#xff08;页表&#xff09;和TLB&#xff08;Translation Lookaside Buffer&#xff09;是内存管理子系统的两大核心组件。作为一位长期从事Linux内核开发的工程师&#xff0c;我经常需要深入理解这两者的工…

作者头像 李华
网站建设 2026/9/10 14:18:31

DanKoe视频笔记法:高效学习与目标实现的系统方法

1. 项目概述&#xff1a;DanKoe视频笔记的核心价值第一次看到DanKoe的视频时&#xff0c;我就被这种独特的"视频笔记"形式吸引了。这不是普通的观影记录&#xff0c;而是一种将视频内容深度内化的系统性方法。通过结构化提取关键观点、建立个人知识关联、设计可执行步…

作者头像 李华