tRPC 类型系统 #
类型系统概览 #
tRPC 的核心优势在于其强大的类型系统。通过 TypeScript 的类型推断,tRPC 实现了真正的端到端类型安全。
text
┌─────────────────────────────────────────────────────────────┐
│ tRPC 类型流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 服务端 │
│ ┌─────────────────────────────────────────────┐ │
│ │ Procedure 定义 │ │
│ │ ├── input: Zod Schema │ │
│ │ ├── output: Zod Schema / 自动推断 │ │
│ │ └── resolver: 函数返回值 │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ AppRouter 类型导出 │ │
│ │ export type AppRouter = typeof appRouter │ │
│ └─────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 客户端 │
│ ┌─────────────────────────────────────────────┐ │
│ │ 类型推断 │ │
│ │ ├── 输入类型自动推断 │ │
│ │ ├── 输出类型自动推断 │ │
│ │ └── 完整的 IDE 支持 │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
类型推断 #
自动推断类型 #
tRPC 会自动从 Zod schema 和 resolver 函数推断类型:
typescript
import { z } from 'zod';
import { publicProcedure, router } from '../trpc';
const userRouter = router({
getById: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
return db.user.findUnique({
where: { id: input.id },
select: {
id: true,
name: true,
email: true,
},
});
}),
});
type Input = { id: string };
type Output = { id: string; name: string; email: string } | null;
text
┌─────────────────────────────────────────────────────────────┐
│ 类型推断流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Input 类型 │
│ .input(z.object({ id: z.string() })) │
│ ↓ │
│ { id: string } │
│ │
│ 2. Output 类型 │
│ .query(async ({ input }) => { │
│ return db.user.findUnique({ ... }) │
│ }) │
│ ↓ │
│ Prisma 返回类型 │
│ │
│ 3. 客户端获得完整类型 │
│ const user = await client.user.getById.query({ id: '1' })│
│ // user: { id: string; name: string; email: string } | null│
│ │
└─────────────────────────────────────────────────────────────┘
inferRouterInputs / inferRouterOutputs #
从路由器推断输入输出类型:
typescript
import type {
inferRouterInputs,
inferRouterOutputs,
} from '@trpc/server';
import type { AppRouter } from '../server/routers';
type RouterInput = inferRouterInputs<AppRouter>;
type RouterOutput = inferRouterOutputs<AppRouter>;
type UserGetByIdInput = RouterInput['user']['getById'];
type UserGetByIdOutput = RouterOutput['user']['getById'];
type UserCreateInput = RouterInput['user']['create'];
type UserCreateOutput = RouterOutput['user']['create'];
实际应用示例 #
typescript
import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
import type { AppRouter } from '../server/routers';
type Input = inferRouterInputs<AppRouter>;
type Output = inferRouterOutputs<AppRouter>;
interface UserFormProps {
user?: Output['user']['getById'];
onSubmit: (data: Input['user']['create']) => void;
}
function UserForm({ user, onSubmit }: UserFormProps) {
return (
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
onSubmit({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
}}>
<input name="name" defaultValue={user?.name} />
<input name="email" defaultValue={user?.email} />
<button type="submit">Submit</button>
</form>
);
}
Zod 类型验证 #
基本类型 #
typescript
import { z } from 'zod';
const schemas = {
string: z.string(),
number: z.number(),
boolean: z.boolean(),
date: z.date(),
null: z.null(),
undefined: z.undefined(),
any: z.any(),
unknown: z.unknown(),
never: z.never(),
void: z.void(),
};
字符串验证 #
typescript
const stringSchemas = {
email: z.string().email(),
url: z.string().url(),
uuid: z.string().uuid(),
regex: z.string().regex(/^[a-z]+$/),
min: z.string().min(1),
max: z.string().max(100),
length: z.string().length(10),
nonempty: z.string().min(1),
trim: z.string().trim(),
lowercase: z.string().toLowerCase(),
uppercase: z.string().toUpperCase(),
};
数字验证 #
typescript
const numberSchemas = {
min: z.number().min(0),
max: z.number().max(100),
positive: z.number().positive(),
negative: z.number().negative(),
integer: z.number().int(),
float: z.number(),
range: z.number().min(0).max(100),
};
对象验证 #
typescript
const userSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email'),
age: z.number().min(0).max(150).optional(),
role: z.enum(['user', 'admin']).default('user'),
address: z.object({
street: z.string(),
city: z.string(),
country: z.string(),
}).optional(),
tags: z.array(z.string()).default([]),
metadata: z.record(z.string(), z.any()).optional(),
});
复杂验证 #
typescript
const schemas = {
array: z.array(z.string()),
tuple: z.tuple([z.string(), z.number()]),
union: z.union([z.string(), z.number()]),
discriminatedUnion: z.discriminatedUnion('type', [
z.object({ type: z.literal('a'), a: z.string() }),
z.object({ type: z.literal('b'), b: z.number() }),
]),
record: z.record(z.string(), z.number()),
map: z.map(z.string(), z.number()),
set: z.set(z.string()),
promise: z.promise(z.string()),
lazy: z.lazy(() => userSchema),
instanceof: z.instanceof(Date),
};
自定义验证 #
typescript
const passwordSchema = z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Must contain uppercase letter')
.regex(/[a-z]/, 'Must contain lowercase letter')
.regex(/[0-9]/, 'Must contain number')
.regex(/[^A-Za-z0-9]/, 'Must contain special character');
const dateRangeSchema = z.object({
start: z.date(),
end: z.date(),
}).refine(
(data) => data.end > data.start,
{ message: 'End date must be after start date' }
);
const userCreateSchema = z.object({
email: z.string().email(),
confirmPassword: z.string(),
password: passwordSchema,
}).refine(
(data) => data.password === data.confirmPassword,
{ message: 'Passwords do not match', path: ['confirmPassword'] }
);
类型提取 #
typescript
import { z } from 'zod';
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
age: z.number().optional(),
});
type User = z.infer<typeof userSchema>;
type UserInput = z.input<typeof userSchema>;
type UserOutput = z.output<typeof userSchema>;
type UserPartial = z.infer<ReturnType<typeof userSchema.partial>>;
type UserRequired = z.infer<ReturnType<typeof userSchema.required>>;
type UserPick = z.infer<ReturnType<typeof userSchema.pick<{ name: true }>>>;
type UserOmit = z.infer<ReturnType<typeof userSchema.omit<{ age: true }>>>;
text
┌─────────────────────────────────────────────────────────────┐
│ Zod 类型提取 │
├─────────────────────────────────────────────────────────────┤
│ │
│ z.infer<T> │
│ - 提取完整的 TypeScript 类型 │
│ - 最常用的类型提取方式 │
│ │
│ z.input<T> │
│ - 提取输入类型 │
│ - 包含默认值之前的类型 │
│ │
│ z.output<T> │
│ - 提取输出类型 │
│ - 包含转换后的类型 │
│ │
│ 示例: │
│ const schema = z.object({ │
│ name: z.string().default('John'), │
│ count: z.number().transform(n => n * 2), │
│ }); │
│ │
│ input: { name?: string; count: number } │
│ output: { name: string; count: number } │
│ │
└─────────────────────────────────────────────────────────────┘
类型共享策略 #
Monorepo 共享 #
text
packages/
├── api/ # 后端包
│ ├── src/
│ │ ├── routers/
│ │ │ └── index.ts
│ │ └── trpc.ts
│ └── package.json
│
├── web/ # 前端包
│ ├── src/
│ │ ├── utils/
│ │ │ └── trpc.ts
│ │ └── App.tsx
│ └── package.json
│
└── shared/ # 共享包
├── src/
│ ├── types.ts # 共享类型
│ └── schemas.ts # 共享 Schema
└── package.json
共享类型包 #
typescript
import { z } from 'zod';
export const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
createdAt: z.date(),
updatedAt: z.date(),
});
export const createUserSchema = userSchema.omit({
id: true,
createdAt: true,
updatedAt: true,
});
export const updateUserSchema = createUserSchema.partial();
export type User = z.infer<typeof userSchema>;
export type CreateUserInput = z.infer<typeof createUserSchema>;
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
后端使用 #
typescript
import { publicProcedure, router } from '../trpc';
import { userSchema, createUserSchema, updateUserSchema } from '@shared/schemas';
export const userRouter = router({
list: publicProcedure.query(async () => {
return db.user.findMany();
}),
getById: publicProcedure
.input(z.object({ id: z.string() }))
.output(userSchema.nullable())
.query(async ({ input }) => {
return db.user.findUnique({ where: { id: input.id } });
}),
create: publicProcedure
.input(createUserSchema)
.output(userSchema)
.mutation(async ({ input }) => {
return db.user.create({ data: input });
}),
update: publicProcedure
.input(z.object({
id: z.string(),
data: updateUserSchema,
}))
.output(userSchema)
.mutation(async ({ input }) => {
return db.user.update({
where: { id: input.id },
data: input.data,
});
}),
});
前端使用 #
typescript
import type { User, CreateUserInput, UpdateUserInput } from '@shared/schemas';
function UserForm({ user, onSubmit }: {
user?: User;
onSubmit: (data: CreateUserInput) => void;
}) {
return (
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
onSubmit({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
}}>
<input name="name" defaultValue={user?.name} />
<input name="email" defaultValue={user?.email} />
<button type="submit">Submit</button>
</form>
);
}
Context 类型 #
定义 Context #
typescript
import { initTRPC, TRPCError } from '@trpc/server';
import * as trpcExpress from '@trpc/server/adapters/express';
interface User {
id: string;
name: string;
role: 'user' | 'admin';
}
interface Context {
user: User | null;
req: trpcExpress.CreateExpressContextOptions['req'];
res: trpcExpress.CreateExpressContextOptions['res'];
}
export async function createContext({
req,
res,
}: trpcExpress.CreateExpressContextOptions): Promise<Context> {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return { user: null, req, res };
}
try {
const user = await verifyToken(token);
return { user, req, res };
} catch {
return { user: null, req, res };
}
}
const t = initTRPC.context<Context>().create();
使用 Context #
typescript
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
user: ctx.user,
},
});
});
export const protectedProcedure = t.procedure.use(isAuthed);
export const userRouter = t.router({
getProfile: protectedProcedure.query(({ ctx }) => {
return ctx.user;
}),
updateProfile: protectedProcedure
.input(z.object({ name: z.string() }))
.mutation(async ({ ctx, input }) => {
return db.user.update({
where: { id: ctx.user.id },
data: input,
});
}),
});
Context 类型推断 #
typescript
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {
user: ctx.user,
},
});
});
const protectedProcedure = t.procedure.use(isAuthed);
protectedProcedure.query(({ ctx }) => {
ctx.user;
});
Meta 类型 #
定义 Meta #
typescript
interface Meta {
requiresAuth?: boolean;
requiredRole?: 'user' | 'admin';
rateLimit?: {
max: number;
windowMs: number;
};
}
const t = initTRPC
.context<Context>()
.meta<Meta>()
.create();
使用 Meta #
typescript
const authMiddleware = t.middleware(({ meta, ctx, next }) => {
if (meta?.requiresAuth && !ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
if (meta?.requiredRole && ctx.user?.role !== meta.requiredRole) {
throw new TRPCError({ code: 'FORBIDDEN' });
}
return next();
});
export const publicProcedure = t.procedure.use(authMiddleware);
export const userRouter = t.router({
list: publicProcedure
.meta({ requiresAuth: true })
.query(async () => {
return db.user.findMany();
}),
delete: publicProcedure
.meta({ requiresAuth: true, requiredRole: 'admin' })
.input(z.object({ id: z.string() }))
.mutation(async ({ input }) => {
return db.user.delete({ where: { id: input.id } });
}),
});
Transformer #
SuperJSON #
SuperJSON 允许传输更多类型,如 Date、Map、Set 等:
typescript
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';
export const t = initTRPC.create({
transformer: superjson,
});
客户端配置 #
typescript
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import superjson from 'superjson';
export const client = createTRPCProxyClient<AppRouter>({
transformer: superjson,
links: [
httpBatchLink({
url: 'http://localhost:4000/trpc',
}),
],
});
支持的类型 #
text
┌─────────────────────────────────────────────────────────────┐
│ SuperJSON 支持的类型 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 原生 JSON: │
│ - string, number, boolean, null │
│ - Array, Object │
│ │
│ SuperJSON 扩展: │
│ - Date │
│ - Map<K, V> │
│ - Set<T> │
│ - RegExp │
│ - Error │
│ - BigInt │
│ - undefined │
│ - Infinity, -Infinity, NaN │
│ │
│ 使用示例: │
│ const router = t.router({ │
│ getDate: t.procedure.query(() => new Date()), │
│ getMap: t.procedure.query(() => new Map([['a', 1]])), │
│ getSet: t.procedure.query(() => new Set([1, 2, 3])), │
│ }); │
│ │
└─────────────────────────────────────────────────────────────┘
类型工具 #
提取路由类型 #
typescript
import type { AnyRouter, Procedure } from '@trpc/server';
type ExtractProcedures<TRouter extends AnyRouter> = TRouter['_def']['record'];
type GetProcedure<TRouter extends AnyRouter, TPath extends string> =
TPath extends keyof ExtractProcedures<TRouter>
? ExtractProcedures<TRouter>[TPath]
: never;
提取过程类型 #
typescript
import type { ProcedureParams } from '@trpc/server';
type GetInput<TProcedure extends Procedure> =
TProcedure extends Procedure<any, any, infer TParams>
? TParams extends ProcedureParams<any, infer TInput, any, any>
? TInput
: never
: never;
type GetOutput<TProcedure extends Procedure> =
TProcedure extends Procedure<any, any, infer TParams>
? TParams extends ProcedureParams<any, any, infer TOutput, any>
? TOutput
: never
: never;
实用类型工具 #
typescript
import type {
inferRouterInputs,
inferRouterOutputs,
inferRouterProxyClient,
} from '@trpc/server';
import type { AppRouter } from '../server/routers';
type RouterInput = inferRouterInputs<AppRouter>;
type RouterOutput = inferRouterOutputs<AppRouter>;
type RouterProxy = inferRouterProxyClient<AppRouter>;
type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};
type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
type OptionalKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];
type RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>;
类型安全最佳实践 #
1. 始终导出类型 #
typescript
export const appRouter = router({
user: userRouter,
post: postRouter,
});
export type AppRouter = typeof appRouter;
2. 使用 Zod 进行验证 #
typescript
const userRouter = router({
create: publicProcedure
.input(createUserSchema)
.output(userSchema)
.mutation(async ({ input }) => {
return db.user.create({ data: input });
}),
});
3. 共享 Schema #
typescript
export const paginationSchema = z.object({
limit: z.number().min(1).max(100).default(10),
offset: z.number().min(0).default(0),
cursor: z.string().optional(),
});
export type Pagination = z.infer<typeof paginationSchema>;
4. 使用类型推断 #
typescript
import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server';
type Input = inferRouterInputs<AppRouter>;
type Output = inferRouterOutputs<AppRouter>;
function useUser(id: string) {
return trpc.user.getById.useQuery({ id });
}
5. 避免使用 any #
typescript
const router = t.router({
bad: t.procedure.query(async () => {
return 'anything' as any;
}),
good: t.procedure
.output(z.string())
.query(async () => {
return 'specific type';
}),
});
下一步 #
现在你已经掌握了 tRPC 的类型系统,接下来学习 中间件,了解如何扩展 tRPC 的功能!
最后更新:2026-03-29