mcpField
T
trpc-typed-form-data

trpc-typed-form-data

github.com/falcondev-oss开发279 次安装4 天前更新
代码仓库

关于 trpc-typed-form-data

@falcondev-oss/trpc-typed-form-data 为 tRPC 添加有类型的 FormData 上传:将非文件字段序列化为 JSON 附加字段,文件仍作为 multipart 部分传输。它适用于需要在客户端和服务端验证上传数据的 tRPC 应用,也支持 React Native 共享模式。

主要功能

  1. 01将非文件字段序列化为 FormData 附加字段
  2. 02让上传文件保留为 multipart 部分
  3. 03提供客户端链接和服务端中间件
  4. 04通过模式验证字段与文件
  5. 05包含文件验证器和 ReactNativeFile 支持

适用场景

  1. 01对包含文件的 tRPC mutation 负载进行类型检查
  2. 02同时验证上传字段和文件
  3. 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 部分传输。客户端、服务端和架构分别负责这项技巧的一部分。

四个入口:

导入路径提供内容
.../clientcreateTypedFormData、typedFormDataLink、ReactNativeFile
.../servertypedFormData、createTypedFormDataPlugin
.../zodzod 原生的 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。

评论

支持 Markdown