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