type-safety-validation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Type Safety & Validation

类型安全与验证

Overview

概述

End-to-end type safety ensures bugs are caught at compile time, not runtime. This skill covers Zod for runtime validation, tRPC for type-safe APIs, Prisma for type-safe database access, and modern TypeScript features.
When to use this skill:
  • Building type-safe APIs (REST, RPC, GraphQL)
  • Validating user input and external data
  • Ensuring database queries are type-safe
  • Creating end-to-end typed full-stack applications
  • Migrating from JavaScript to TypeScript
  • Implementing strict validation rules
端到端类型安全可确保在编译时而非运行时捕获bug。本技能涵盖用于运行时验证的Zod、用于类型安全API的tRPC、用于类型安全数据库访问的Prisma,以及现代TypeScript特性。
何时使用本技能:
  • 构建类型安全的API(REST、RPC、GraphQL)
  • 验证用户输入和外部数据
  • 确保数据库查询的类型安全
  • 创建端到端类型化的全栈应用
  • 从JavaScript迁移到TypeScript
  • 实施严格的验证规则

Core Stack

核心技术栈

1. Zod - Runtime Validation

1. Zod - 运行时验证

typescript
import { z } from 'zod'

// Define schema
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())
})

// Infer TypeScript type from schema
type User = z.infer<typeof UserSchema>

// Validate data
const result = UserSchema.safeParse(data)
if (result.success) {
  const user: User = result.data
} else {
  console.error(result.error.issues)
}

// Transform data
const EmailSchema = z.string().email().transform(email => email.toLowerCase())
Advanced Patterns:
typescript
// Refinements
const PasswordSchema = z.string()
  .min(8)
  .refine((pass) => /[A-Z]/.test(pass), 'Must contain uppercase')
  .refine((pass) => /[0-9]/.test(pass), 'Must contain number')

// Discriminated Unions
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() })
])

// Recursive Types
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
  z.object({
    name: z.string(),
    children: z.array(CategorySchema).optional()
  })
)
typescript
import { z } from 'zod'

// Define schema
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())
})

// Infer TypeScript type from schema
type User = z.infer<typeof UserSchema>

// Validate data
const result = UserSchema.safeParse(data)
if (result.success) {
  const user: User = result.data
} else {
  console.error(result.error.issues)
}

// Transform data
const EmailSchema = z.string().email().transform(email => email.toLowerCase())
高级模式:
typescript
// Refinements
const PasswordSchema = z.string()
  .min(8)
  .refine((pass) => /[A-Z]/.test(pass), 'Must contain uppercase')
  .refine((pass) => /[0-9]/.test(pass), 'Must contain number')

// Discriminated Unions
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() })
])

// Recursive Types
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
  z.object({
    name: z.string(),
    children: z.array(CategorySchema).optional()
  })
)

2. tRPC - Type-Safe APIs

2. tRPC - 类型安全API

typescript
// Server: Define procedures
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

// Client: Fully typed!
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'
import type { AppRouter } from './server'

const client = createTRPCProxyClient<AppRouter>({
  links: [httpBatchLink({ url: 'http://localhost:3000/api/trpc' })]
})

// TypeScript knows the exact shape!
const user = await client.getUser.query({ id: '123' })
//    ^? User | null
typescript
// Server: Define procedures
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

// Client: Fully typed!
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'
import type { AppRouter } from './server'

const client = createTRPCProxyClient<AppRouter>({
  links: [httpBatchLink({ url: 'http://localhost:3000/api/trpc' })]
})

// TypeScript knows the exact shape!
const user = await client.getUser.query({ id: '123' })
//    ^? User | null

3. Prisma - Type-Safe ORM

3. Prisma - 类型安全ORM

prisma
// 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
}
typescript
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

// Fully typed queries
const user = await prisma.user.findUnique({
  where: { id: '123' },
  include: {
    posts: {
      where: { published: true },
      orderBy: { createdAt: 'desc' }
    }
  }
})
// user is typed as: User & { posts: Post[] }

// Type-safe creates
const newUser = await prisma.user.create({
  data: {
    email: 'user@example.com',
    posts: {
      create: [
        { title: 'First Post', content: 'Hello world' }
      ]
    }
  }
})
prisma
// 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
}
typescript
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

// Fully typed queries
const user = await prisma.user.findUnique({
  where: { id: '123' },
  include: {
    posts: {
      where: { published: true },
      orderBy: { createdAt: 'desc' }
    }
  }
})
// user is typed as: User & { posts: Post[] }

// Type-safe creates
const newUser = await prisma.user.create({
  data: {
    email: 'user@example.com',
    posts: {
      create: [
        { title: 'First Post', content: 'Hello world' }
      ]
    }
  }
})

4. TypeScript 5.7+ Features

4. TypeScript 5.7+ 特性

typescript
// Const type parameters (TS 5.0+)
function firstElement<T extends readonly any[]>(arr: T) {
  return arr[0]
}

const result = firstElement(['a', 'b'] as const)
// result is typed as 'a'

// Satisfies operator (TS 4.9+)
const config = {
  url: 'https://api.example.com',
  timeout: 5000
} satisfies Config  // Ensures config matches Config, but keeps literal types

// Decorators (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() {}
}
typescript
// Const type parameters (TS 5.0+)
function firstElement<T extends readonly any[]>(arr: T) {
  return arr[0]
}

const result = firstElement(['a', 'b'] as const)
// result is typed as 'a'

// Satisfies operator (TS 4.9+)
const config = {
  url: 'https://api.example.com',
  timeout: 5000
} satisfies Config  // Ensures config matches Config, but keeps literal types

// Decorators (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() {}
}

Full-Stack Example

全栈示例

typescript
// ===== BACKEND (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
        })
      })
  }
})

// ===== FRONTEND (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>
  )
}
typescript
// ===== 后端(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>
  )
}

Best Practices

最佳实践

Validation

验证

  • ✅ Validate at boundaries (API inputs, form submissions, external data)
  • ✅ Use
    .safeParse()
    to handle errors gracefully
  • ✅ Provide clear error messages for users
  • ✅ Validate environment variables at startup
  • ✅ Use branded types for IDs (
    z.string().brand<'UserId'>()
    )
  • ✅ 在边界处进行验证(API输入、表单提交、外部数据)
  • ✅ 使用
    .safeParse()
    优雅地处理错误
  • ✅ 为用户提供清晰的错误提示
  • ✅ 在启动时验证环境变量
  • ✅ 为ID使用品牌类型(
    z.string().brand<'UserId'>()

Type Safety

类型安全

  • ✅ Enable
    strict: true
    in
    tsconfig.json
  • ✅ Use
    noUncheckedIndexedAccess
    for safer array access
  • ✅ Prefer
    unknown
    over
    any
  • ✅ Use type guards for narrowing
  • ✅ Leverage inference with
    typeof
    and
    ReturnType
  • ✅ 在
    tsconfig.json
    中启用
    strict: true
  • ✅ 使用
    noUncheckedIndexedAccess
    实现更安全的数组访问
  • ✅ 优先使用
    unknown
    而非
    any
  • ✅ 使用类型守卫进行类型收窄
  • ✅ 借助
    typeof
    ReturnType
    进行类型推断

Performance

性能

  • ✅ Reuse schemas (don't create inline)
  • ✅ Use
    .parse()
    for known-good data (faster than
    .safeParse()
    )
  • ✅ Enable Prisma query optimization
  • ✅ Use tRPC batching for multiple queries
  • ✅ Cache validation results when appropriate
  • ✅ 复用Schema(不要内联创建)
  • ✅ 对已知合法数据使用
    .parse()
    (比
    .safeParse()
    更快)
  • ✅ 启用Prisma查询优化
  • ✅ 使用tRPC批处理处理多个查询
  • ✅ 适当缓存验证结果

Resources

参考资源