name: type-safety-validation description: 使用Zod运行时验证、tRPC类型安全API、Prisma ORM以及TypeScript 5.7+特性实现端到端类型安全。为2025年及以后的开发构建从数据库到UI的完全类型安全应用程序。 version: 1.0.0 author: AI Agent Hub tags: [typescript, zod, trpc, prisma, type-safety, validation, 2025]
类型安全与验证
概述
端到端类型安全确保错误在编译时被捕获,而不是在运行时。本技能涵盖Zod用于运行时验证、tRPC用于类型安全API、Prisma用于类型安全数据库访问以及现代TypeScript特性。
何时使用此技能:
- 构建类型安全的API(REST、RPC、GraphQL)
- 验证用户输入和外部数据
- 确保数据库查询是类型安全的
- 创建端到端类型化的全栈应用程序
- 从JavaScript迁移到TypeScript
- 实现严格的验证规则
核心技术栈
1. Zod - 运行时验证
import { z } from 'zod'
// 定义模式
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
age: z.number().int().positive().max(120),
role: z.enum(['admin', 'user', 'guest']),
metadata: z.record(z.string()).optional(),
createdAt: z.date().default(() => new Date())
})
// 从模式推断TypeScript类型
type User = z.infer<typeof UserSchema>
// 验证数据
const result = UserSchema.safeParse(data)
if (result.success) {
const user: User = result.data
} else {
console.error(result.error.issues)
}
// 转换数据
const EmailSchema = z.string().email().transform(email => email.toLowerCase())
高级模式:
// 细化
const PasswordSchema = z.string()
.min(8)
.refine((pass) => /[A-Z]/.test(pass), '必须包含大写字母')
.refine((pass) => /[0-9]/.test(pass), '必须包含数字')
// 可辨识联合
const EventSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('click'), x: z.number(), y: z.number() }),
z.object({ type: z.literal('scroll'), offset: z.number() })
])
// 递归类型
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
z.object({
name: z.string(),
children: z.array(CategorySchema).optional()
})
)
2. tRPC - 类型安全API
// 服务器端:定义过程
import { initTRPC } from '@trpc/server'
import { z } from 'zod'
const t = initTRPC.create()
export const appRouter = t.router({
getUser: t.procedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
return await db.user.findUnique({ where: { id: input.id } })
}),
createUser: t.procedure
.input(z.object({
email: z.string().email(),
name: z.string()
}))
.mutation(async ({ input }) => {
return await db.user.create({ data: input })
})
})
export type AppRouter = typeof appRouter
// 客户端:完全类型化!
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'
import type { AppRouter } from './server'
const client = createTRPCProxyClient<AppRouter>({
links: [httpBatchLink({ url: 'http://localhost:3000/api/trpc' })]
})
// TypeScript知道确切的形状!
const user = await client.getUser.query({ id: '123' })
// ^? User | null
3. Prisma - 类型安全ORM
// schema.prisma
model User {
id String @id @default(cuid())
email String @unique
posts Post[]
profile Profile?
createdAt DateTime @default(now())
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
}
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
// 完全类型化的查询
const user = await prisma.user.findUnique({
where: { id: '123' },
include: {
posts: {
where: { published: true },
orderBy: { createdAt: 'desc' }
}
}
})
// user 被类型化为: User & { posts: Post[] }
// 类型安全的创建
const newUser = await prisma.user.create({
data: {
email: 'user@example.com',
posts: {
create: [
{ title: 'First Post', content: 'Hello world' }
]
}
}
})
4. TypeScript 5.7+ 特性
// 常量类型参数 (TS 5.0+)
function firstElement<T extends readonly any[]>(arr: T) {
return arr[0]
}
const result = firstElement(['a', 'b'] as const)
// result 被类型化为 'a'
// Satisfies 操作符 (TS 4.9+)
const config = {
url: 'https://api.example.com',
timeout: 5000
} satisfies Config // 确保config匹配Config,但保留字面量类型
// 装饰器 (TS 5.0+)
function logged(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey}`)
return original.apply(this, args)
}
}
class API {
@logged
async fetchData() {}
}
全栈示例
// ===== 后端 (Next.js API) =====
// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
import { appRouter } from '@/server/routers/_app'
export async function GET(req: Request) {
return fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext: () => ({})
})
}
export const POST = GET
// server/routers/_app.ts
import { z } from 'zod'
import { prisma } from '@/lib/prisma'
import { publicProcedure, router } from '../trpc'
export const appRouter = router({
posts: {
list: publicProcedure
.input(z.object({
limit: z.number().min(1).max(100).default(10),
cursor: z.string().optional()
}))
.query(async ({ input }) => {
const posts = await prisma.post.findMany({
take: input.limit + 1,
cursor: input.cursor ? { id: input.cursor } : undefined,
orderBy: { createdAt: 'desc' },
include: { author: true }
})
return {
items: posts.slice(0, input.limit),
nextCursor: posts[input.limit]?.id
}
}),
create: publicProcedure
.input(z.object({
title: z.string().min(1).max(200),
content: z.string().optional()
}))
.mutation(async ({ input }) => {
return await prisma.post.create({
data: input
})
})
}
})
// ===== 前端 (React) =====
// lib/trpc.ts
import { createTRPCReact } from '@trpc/react-query'
import type { AppRouter } from '@/server/routers/_app'
export const trpc = createTRPCReact<AppRouter>()
// components/PostList.tsx
'use client'
import { trpc } from '@/lib/trpc'
export function PostList() {
const { data, isLoading } = trpc.posts.list.useQuery({ limit: 10 })
const createPost = trpc.posts.create.useMutation()
if (isLoading) return <div>Loading...</div>
return (
<div>
{data?.items.map(post => (
<div key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
<span>By {post.author.name}</span>
</div>
))}
<button onClick={() => createPost.mutate({ title: 'New Post' })}>
Create Post
</button>
</div>
)
}
最佳实践
验证
- ✅ 在边界处验证(API输入、表单提交、外部数据)
- ✅ 使用
.safeParse()优雅地处理错误 - ✅ 为用户提供清晰的错误信息
- ✅ 在启动时验证环境变量
- ✅ 为ID使用品牌化类型(
z.string().brand<'UserId'>())
类型安全
- ✅ 在
tsconfig.json中启用strict: true - ✅ 使用
noUncheckedIndexedAccess进行更安全的数组访问 - ✅ 优先使用
unknown而不是any - ✅ 使用类型守卫进行类型收窄
- ✅ 利用
typeof和ReturnType进行类型推断
性能
- ✅ 重用模式(不要内联创建)
- ✅ 对于已知良好的数据使用
.parse()(比.safeParse()更快) - ✅ 启用Prisma查询优化
- ✅ 对多个查询使用tRPC批处理
- ✅ 在适当的时候缓存验证结果