关于 trpc-typed-form-data
@falcondev-oss/trpc-typed-form-data 为 tRPC 添加有类型的 FormData 上传:将非文件字段序列化为 JSON 附加字段,文件仍作为 multipart 部分传输。它适用于需要在客户端和服务端验证上传数据的 tRPC 应用,也支持 React Native 共享模式。
主要功能
- 01将非文件字段序列化为 FormData 附加字段
- 02让上传文件保留为 multipart 部分
- 03提供客户端链接和服务端中间件
- 04通过模式验证字段与文件
- 05包含文件验证器和 ReactNativeFile 支持
适用场景
- 01对包含文件的 tRPC mutation 负载进行类型检查
- 02同时验证上传字段和文件
- 03在服务端与 React Native 客户端间共享文件模式
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ falcondev-oss/ skills --skill trpc-typed-form-data安装后在 Claude Code 中输入/trpc-typed-form-data确认已加载。
附带文件reference/react-native.md
详细介绍
@falcondev-oss/trpc-typed-form-data
tRPC 无法为 FormData 输入提供类型:文件必须作为 multipart 部分传输,其他内容都是无类型字符串,而且 input 会是 undefined。此软件包通过将非文件字段作为序列化的旁路数据传递,恢复端到端类型安全——即在一个额外的 FormData 字段(~data)中放入 JSON 数据块,同时文件仍作为真正的 multipart 部分传输。客户端、服务端和架构分别负责这项技巧的一部分。
四个入口:
| 导入路径 | 提供内容 |
|---|---|
.../client | createTypedFormData、typedFormDataLink、ReactNativeFile |
.../server | typedFormData、createTypedFormDataPlugin |
.../zod | zod 原生的 file() 验证器(嵌套在 z.object 中) |
.(根路径) | file(Standard Schema)、isFile、类型——与框架无关的子集 |
file、isFile 和类型(FileValue、FileValidationOptions、TypedFormData)也会从 /client 和 /server 重新导出,因此可以从你当前所在的位置导入。
三个接入点——缺一不可
只有接入所有部分,旁路数据才能生效。漏掉任何一个,验证都会抛出一条指出缺失项的消息。
1. 客户端链接——将旁路数据序列化到 FormData 中。将其放在终止链接之前:
import { typedFormDataLink } from '@falcondev-oss/trpc-typed-form-data/client'
import { createTRPCClient, httpLink } from '@trpc/client'
const trpc = createTRPCClient<AppRouter>({
links: [
typedFormDataLink(), // 👈 before httpLink
httpLink({ url: '/api/trpc' }),
],
})
2. 服务端中间件——将旁路数据反序列化并写回 FormData。从你的 tRPC 实例创建插件,并将其中间件 concat 到接受上传的过程上:
import { createTypedFormDataPlugin } from '@falcondev-oss/trpc-typed-form-data/server'
const t = initTRPC.create()
const typedFormDataPlugin = createTypedFormDataPlugin(t)
export const uploadProcedure = t.procedure.concat(typedFormDataPlugin.middleware)
3. 输入架构包装器——用 typedFormData() 包装输入架构。它会读取旁路数据和文件部分,并一起验证:
import { typedFormData } from '@falcondev-oss/trpc-typed-form-data/server'
import { z } from 'zod'
export const router = t.router({
upload: uploadProcedure
.input(
typedFormData(
z.object({
userId: z.string(),
file: z.instanceof(File),
}),
),
)
.mutation(({ input }) => {
// input is fully typed: { userId: string; file: File }
}),
})
调用方式——使用 createTypedFormData 构建载荷,并直接传给 .mutate();字段会根据架构进行类型检查:
import { createTypedFormData } from '@falcondev-oss/trpc-typed-form-data/client'
await trpc.upload.mutate(
createTypedFormData({
userId: '123',
file: new File(['contents'], 'example.txt'),
}),
)
文件数组也能正常工作:在某个键下传入 File[]——每个文件都会追加到该键下,并通过 getAll 读回。
文件验证——优先使用 file() 而不是 z.file()
如果架构需要与 React Native / Expo 客户端共享,请使用此软件包的 file()。在没有 DOM File 的环境(React Native)中,z.file() 推断出的类型会降级为 any,从而在 tRPC 服务端→客户端边界上悄然失去类型检查。file() 在任何环境中都会推断出稳定的 FileValue——它是 Web File/Blob API 的结构化子集(name、size、type、arrayBuffer、stream、slice、text)。
两种形式使用相同的选项({ maxSize?, minSize?, mimeTypes? },均为可选项,字节限制包含边界值):
// zod stack — nests inside z.object like z.file()
import { file } from '@falcondev-oss/trpc-typed-form-data/zod'
typedFormData(z.object({
avatar: file({ maxSize: 10_000_000, mimeTypes: ['image/png', 'image/jpeg'] }),
}))
// non-zod / any Standard Schema stack
import { file } from '@falcondev-oss/trpc-typed-form-data/server'
isFile(value) 是一个类型守卫,可将 unknown 收窄为 FileValue,适用于架构之外的临时检查。对于从这些架构中得到的值(过程 input、上传辅助函数),请使用 FileValue 而不是 File 标注。
注意事项
- 三个接入点缺一不可。 缺少链接、中间件或包装器都会产生不同的验证错误——请阅读错误消息,其中会指出缺失的部分。
- 转换器必须匹配。 如果你的路由器使用转换器(例如 superjson),请将其传给
typedFormDataLink({ transformer })——旁路数据由链接的转换器序列化,并由路由器的转换器反序列化。不匹配会导致服务端抛出BAD_REQUEST。 - 自定义传输字段。 默认情况下,旁路数据通过
~data字段传输。使用transferDataKey覆盖——在typedFormDataLink({ transferDataKey })和createTypedFormDataPlugin(t, { transferDataKey })中设置相同的值。 - FormData 不能批处理。 上传路径应以
httpLink终止,而不是httpBatchLink。 - 中间件只会在没有普通
input的变更操作上触发(即 FormData 路径)。查询和订阅会原样通过。
React Native / Expo
从图像/文档选择器上传时需要使用 ReactNativeFile——RN 的 FormData 只会发送带有 uri 的部分,而且不会读取 Blob 的字节。接入 Expo 上传前,请阅读 reference/react-native.md。