AgentSkillsCN

typescript-chatbot-types

在Zod运行时验证的基础上,为聊天机器人应用打造TypeScript类型安全模式。当您需要定义API Schema、创建类型安全的组件、验证用户输入,或处理LLM的响应时,此技能将助您事半功倍。

SKILL.md
--- frontmatter
name: typescript-chatbot-types
description: TypeScript type safety patterns for chatbot applications with Zod runtime validation. Use when defining API schemas, creating type-safe components, validating user input, or working with LLM responses.
license: MIT
metadata:
  author: camaral-team
  version: "1.0.0"
  language: typescript
  validation: zod

TypeScript Chatbot Types

Type-safe patterns for building chatbot applications with runtime validation using Zod.

When to Apply

Use this skill when:

  • Defining API request/response schemas
  • Creating type-safe React components
  • Validating user input at runtime
  • Working with LLM responses and structured data
  • Building type-safe database queries
  • Ensuring end-to-end type safety

Key Patterns

1. Core Domain Types (CRITICAL)

Pattern: Define strict types for your chatbot domain

typescript
// lib/types/chat.ts

/**
 * Message roles in conversation
 */
export type MessageRole = 'user' | 'assistant' | 'system'

/**
 * A single message in the chat history
 */
export interface Message {
  role: MessageRole
  content: string
  sources?: string[]
  timestamp?: Date
  id?: string
}

/**
 * Request payload for chat API
 */
export interface ChatRequest {
  message: string
  history?: Message[]
  sessionId?: string
}

/**
 * Response from chat API
 */
export interface ChatResponse {
  response: string
  sources: string[]
  metadata?: {
    model: string
    chunks_used?: number
    avg_similarity?: number
    tokens?: number
  }
}

/**
 * Error response structure
 */
export interface ErrorResponse {
  error: string
  code: string
  details?: Record<string, unknown>
}

2. Zod Schemas for Runtime Validation (CRITICAL)

Pattern: Mirror TypeScript types with Zod for runtime safety

typescript
// lib/validation/schemas.ts
import { z } from 'zod'

/**
 * Message schema with validation rules
 */
export const messageSchema = z.object({
  role: z.enum(['user', 'assistant', 'system']),
  content: z.string()
    .min(1, 'Message cannot be empty')
    .max(10000, 'Message too long'),
  sources: z.array(z.string()).optional(),
  timestamp: z.date().optional(),
  id: z.string().uuid().optional()
})

/**
 * Chat request schema
 */
export const chatRequestSchema = z.object({
  message: z.string()
    .min(1, 'Message is required')
    .max(1000, 'Message must be less than 1000 characters')
    .trim(),
  history: z.array(messageSchema)
    .max(20, 'History limited to 20 messages')
    .optional()
    .default([]),
  sessionId: z.string().uuid().optional()
})

/**
 * Chat response schema
 */
export const chatResponseSchema = z.object({
  response: z.string(),
  sources: z.array(z.string()),
  metadata: z.object({
    model: z.string(),
    chunks_used: z.number().optional(),
    avg_similarity: z.number().optional(),
    tokens: z.number().optional()
  }).optional()
})

/**
 * Infer TypeScript types from Zod schemas
 */
export type Message = z.infer<typeof messageSchema>
export type ChatRequest = z.infer<typeof chatRequestSchema>
export type ChatResponse = z.infer<typeof chatResponseSchema>

3. API Route Type Safety (HIGH)

Pattern: Validate and parse requests with detailed error handling

typescript
// app/api/chat/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { chatRequestSchema, type ChatResponse } from '@/lib/validation/schemas'
import { ZodError } from 'zod'

export async function POST(req: NextRequest) {
  try {
    // Parse and validate request body
    const body = await req.json()
    const validatedData = chatRequestSchema.parse(body)
    
    // TypeScript now knows the exact shape
    const { message, history, sessionId } = validatedData
    
    // Generate response (type-safe)
    const response: ChatResponse = await generateChatResponse({
      message,
      history
    })
    
    // Return validated response
    return NextResponse.json(response)
    
  } catch (error) {
    // Handle Zod validation errors
    if (error instanceof ZodError) {
      return NextResponse.json(
        {
          error: 'Invalid request data',
          code: 'VALIDATION_ERROR',
          details: error.errors.map(err => ({
            field: err.path.join('.'),
            message: err.message
          }))
        },
        { status: 400 }
      )
    }
    
    // Handle other errors
    console.error('Chat API error:', error)
    return NextResponse.json(
      { error: 'Internal server error', code: 'INTERNAL_ERROR' },
      { status: 500 }
    )
  }
}

4. Type-Safe Component Props (HIGH)

Pattern: Strictly typed React components with defaults

typescript
// components/MessageBubble.tsx
import { type Message } from '@/lib/types/chat'
import { cn } from '@/lib/utils'

interface MessageBubbleProps {
  message: Message
  className?: string
  showSources?: boolean
  onSourceClick?: (source: string) => void
}

export function MessageBubble({
  message,
  className,
  showSources = true,
  onSourceClick
}: MessageBubbleProps) {
  const isUser = message.role === 'user'
  
  return (
    <div className={cn(
      'message-bubble',
      isUser ? 'user-message' : 'assistant-message',
      className
    )}>
      <p>{message.content}</p>
      
      {showSources && message.sources && message.sources.length > 0 && (
        <div className="sources">
          <span className="sources-label">📚 Basado en:</span>
          <ul>
            {message.sources.map((source, i) => (
              <li 
                key={i}
                onClick={() => onSourceClick?.(source)}
                className={onSourceClick ? 'cursor-pointer hover:underline' : ''}
              >
                {source}
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  )
}

5. Type-Safe Hooks (MEDIUM)

Pattern: Generic hooks with proper typing

typescript
// hooks/useChat.ts
import { useState, useCallback } from 'react'
import { type Message, type ChatRequest, type ChatResponse } from '@/lib/types/chat'

interface UseChatOptions {
  initialMessages?: Message[]
  onError?: (error: Error) => void
}

interface UseChatReturn {
  messages: Message[]
  isLoading: boolean
  error: Error | null
  sendMessage: (content: string) => Promise<void>
  clearMessages: () => void
}

export function useChat(options: UseChatOptions = {}): UseChatReturn {
  const [messages, setMessages] = useState<Message[]>(options.initialMessages || [])
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<Error | null>(null)
  
  const sendMessage = useCallback(async (content: string) => {
    setIsLoading(true)
    setError(null)
    
    // Add user message optimistically
    const userMessage: Message = {
      role: 'user',
      content,
      timestamp: new Date()
    }
    setMessages(prev => [...prev, userMessage])
    
    try {
      // Build request
      const request: ChatRequest = {
        message: content,
        history: messages
      }
      
      // Call API
      const response = await fetch('/api/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(request)
      })
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }
      
      // Parse response (runtime validation would go here)
      const data: ChatResponse = await response.json()
      
      // Add assistant message
      const assistantMessage: Message = {
        role: 'assistant',
        content: data.response,
        sources: data.sources,
        timestamp: new Date()
      }
      setMessages(prev => [...prev, assistantMessage])
      
    } catch (err) {
      const error = err instanceof Error ? err : new Error('Unknown error')
      setError(error)
      options.onError?.(error)
      
      // Remove optimistic user message on error
      setMessages(prev => prev.slice(0, -1))
    } finally {
      setIsLoading(false)
    }
  }, [messages, options])
  
  const clearMessages = useCallback(() => {
    setMessages([])
  }, [])
  
  return {
    messages,
    isLoading,
    error,
    sendMessage,
    clearMessages
  }
}

// Usage in component
function ChatComponent() {
  const { messages, isLoading, sendMessage } = useChat({
    onError: (error) => console.error('Chat error:', error)
  })
  
  // Fully type-safe
  return <div>...</div>
}

6. RAG-Specific Types (MEDIUM)

Pattern: Types for vector search and embeddings

typescript
// lib/types/rag.ts

/**
 * A chunk of text from the knowledge base
 */
export interface Chunk {
  id: string
  text: string
  source_file: string
  metadata: ChunkMetadata
}

/**
 * Metadata associated with a chunk
 */
export interface ChunkMetadata {
  section?: string
  title?: string
  wordCount?: number
  [key: string]: unknown // Allow additional metadata
}

/**
 * A chunk with its embedding vector
 */
export interface ChunkWithEmbedding extends Chunk {
  embedding: number[] // 1536 dimensions for text-embedding-3-small
}

/**
 * Result from semantic search
 */
export interface RetrievalResult {
  text: string
  source_file: string
  similarity: number
  metadata?: ChunkMetadata
}

/**
 * Options for chunking documents
 */
export interface ChunkingOptions {
  chunkSize: number
  overlapSize: number
  splitByHeaders: boolean
}

// Zod schemas for RAG types
import { z } from 'zod'

export const chunkMetadataSchema = z.record(z.unknown()).and(
  z.object({
    section: z.string().optional(),
    title: z.string().optional(),
    wordCount: z.number().optional()
  })
)

export const chunkSchema = z.object({
  id: z.string().uuid(),
  text: z.string().min(1),
  source_file: z.string(),
  metadata: chunkMetadataSchema
})

export const retrievalResultSchema = z.object({
  text: z.string(),
  source_file: z.string(),
  similarity: z.number().min(0).max(1),
  metadata: chunkMetadataSchema.optional()
})

7. Environment Variables (MEDIUM)

Pattern: Type-safe environment variables with validation

typescript
// lib/env.ts
import { z } from 'zod'

/**
 * Environment variables schema
 */
const envSchema = z.object({
  OPENAI_API_KEY: z.string().min(1, 'OpenAI API key is required'),
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
  DATABASE_PATH: z.string().optional().default('./data/vector_store.db')
})

/**
 * Validated environment variables
 */
export const env = envSchema.parse({
  OPENAI_API_KEY: process.env.OPENAI_API_KEY,
  NODE_ENV: process.env.NODE_ENV,
  DATABASE_PATH: process.env.DATABASE_PATH
})

// Usage: Always use `env` instead of `process.env`
import { env } from '@/lib/env'

const openai = new OpenAI({
  apiKey: env.OPENAI_API_KEY // Type-safe, guaranteed to exist
})

Anti-Patterns

❌ Don't: Use any or skip validation

typescript
// BAD: No type safety
async function POST(req: Request) {
  const body: any = await req.json()
  const message = body.message // Could be undefined, wrong type, etc.
}

✅ Do: Validate with Zod

typescript
// GOOD: Full type safety
async function POST(req: Request) {
  const body = await req.json()
  const { message } = chatRequestSchema.parse(body) // Throws if invalid
}

❌ Don't: Use loose types

typescript
// BAD: Too permissive
interface Message {
  role: string // Should be literal union
  content: any  // Should be string
}

✅ Do: Use strict types

typescript
// GOOD: Precise types
interface Message {
  role: 'user' | 'assistant' | 'system'
  content: string
}

Performance Tips

  1. Use type inference - type User = z.infer<typeof userSchema>
  2. Validate once - Don't re-validate the same data
  3. Use discriminated unions - For variant types (user vs assistant messages)
  4. Enable strict mode - In tsconfig.json
  5. Use const assertions - For immutable data

Testing

typescript
// Test Zod schemas
import { describe, it, expect } from 'vitest'
import { chatRequestSchema } from '@/lib/validation/schemas'

describe('chatRequestSchema', () => {
  it('should accept valid request', () => {
    const validRequest = {
      message: 'Hello',
      history: []
    }
    
    expect(() => chatRequestSchema.parse(validRequest)).not.toThrow()
  })
  
  it('should reject empty message', () => {
    const invalidRequest = {
      message: '',
      history: []
    }
    
    expect(() => chatRequestSchema.parse(invalidRequest)).toThrow()
  })
  
  it('should reject too long message', () => {
    const invalidRequest = {
      message: 'a'.repeat(1001),
      history: []
    }
    
    expect(() => chatRequestSchema.parse(invalidRequest)).toThrow()
  })
})

TypeScript Config

json
// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "exactOptionalPropertyTypes": true
  }
}

References