| 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
export type MessageRole = 'user' | 'assistant' | 'system'
export interface Message {
role: MessageRole
content: string
sources?: string[]
timestamp?: Date
id?: string
}
export interface ChatRequest {
message: string
history?: Message[]
sessionId?: string
}
export interface ChatResponse {
response: string
sources: string[]
metadata?: {
model: string
chunks_used?: number
avg_similarity?: number
tokens?: number
}
}
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
import { z } from 'zod'
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()
})
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')
.()
.([]),
: z.().().()
})
chatResponseSchema = z.({
: z.(),
: z.(z.()),
: z.({
: z.(),
: z.().(),
: z.().(),
: z.().()
}).()
})
= z.< messageSchema>
= z.< chatRequestSchema>
= z.< chatResponseSchema>
3. API Route Type Safety (HIGH)
Pattern: Validate and parse requests with detailed error handling
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 {
const body = await req.json()
const validatedData = chatRequestSchema.parse(body)
const { message, history, sessionId } = validatedData
const response: ChatResponse = await generateChatResponse({
message,
history
})
return NextResponse.json(response)
} catch (error) {
if (error instanceof ZodError) {
return NextResponse.(
{
: ,
: ,
: error..( ({
: err..(),
: err.
}))
},
{ : }
)
}
.(, error)
.(
{ : , : },
{ : }
)
}
}
4. Type-Safe Component Props (HIGH)
Pattern: Strictly typed React components with defaults
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 =>📚 Basado en:
{message.sources.map((source, i) => (
onSourceClick?.(source)}
className={onSourceClick ? 'cursor-pointer hover:underline' : ''}
>
{source}
))}
)}
)
}
5. Type-Safe Hooks (MEDIUM)
Pattern: Generic hooks with proper typing
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] = ()
[error, setError] = useState< | >()
sendMessage = ( (: ) => {
()
()
: = {
: ,
content,
: ()
}
( [...prev, userMessage])
{
: = {
: content,
: messages
}
response = (, {
: ,
: { : },
: .(request)
})
(!response.) {
()
}
: = response.()
: = {
: ,
: data.,
: data.,
: ()
}
( [...prev, assistantMessage])
} (err) {
error = err ? err : ()
(error)
options.?.(error)
( prev.(, -))
} {
()
}
}, [messages, options])
clearMessages = ( {
([])
}, [])
{
messages,
isLoading,
error,
sendMessage,
clearMessages
}
}
() {
{ messages, isLoading, sendMessage } = ({
: .(, error)
})
}
6. RAG-Specific Types (MEDIUM)
Pattern: Types for vector search and embeddings
export interface Chunk {
id: string
text: string
source_file: string
metadata: ChunkMetadata
}
export interface ChunkMetadata {
section?: string
title?: string
wordCount?: number
[key: string]: unknown
}
export interface ChunkWithEmbedding extends Chunk {
embedding: number[]
}
export interface RetrievalResult {
text: string
source_file: string
similarity: number
metadata?: ChunkMetadata
}
export {
:
:
:
}
{ z }
chunkMetadataSchema = z.(z.()).(
z.({
: z.().(),
: z.().(),
: z.().()
})
)
chunkSchema = z.({
: z.().(),
: z.().(),
: z.(),
: chunkMetadataSchema
})
retrievalResultSchema = z.({
: z.(),
: z.(),
: z.().().(),
: chunkMetadataSchema.()
})
7. Environment Variables (MEDIUM)
Pattern: Type-safe environment variables with validation
import { z } from 'zod'
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')
})
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
})
import { env } from '@/lib/env'
const openai = new OpenAI({
apiKey: env.OPENAI_API_KEY
})
Anti-Patterns
❌ Don't: Use any or skip validation
async function POST(req: Request) {
const body: any = await req.json()
const message = body.message
}
✅ Do: Validate with Zod
async function POST(req: Request) {
const body = await req.json()
const { message } = chatRequestSchema.parse(body)
}
❌ Don't: Use loose types
interface Message {
role: string
content: any
}
✅ Do: Use strict types
interface Message {
role: 'user' | 'assistant' | 'system'
content: string
}
Performance Tips
- Use type inference -
type User = z.infer<typeof userSchema>
- Validate once - Don't re-validate the same data
- Use discriminated unions - For variant types (user vs assistant messages)
- Enable strict mode - In tsconfig.json
- Use const assertions - For immutable data
Testing
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
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true
}
}
References