10 TypeScript Patterns Every AI Developer Should Know

TL;DR
The TypeScript patterns that show up in every AI project. Streaming responses, type-safe tool definitions, structured output, retry logic, and more.
| Official Sources | |
|---|---|
| Vercel AI SDK Documentation | Core SDK for streaming, tools, and structured output |
| Zod Documentation | Runtime schema validation and TypeScript inference |
| TypeScript Handbook | Generics, discriminated unions, and type inference |
| Anthropic TypeScript SDK | Official Claude API client |
| OpenAI Node SDK | Official OpenAI API client |
| MDN AsyncIterator | Native streaming patterns in JavaScript |
These are the patterns I reach for in every AI project. Not theoretical - these show up in real TypeScript codebases that ship AI features.
1. Streaming with AsyncIterator#
Every AI response should stream. Users see output immediately instead of waiting for the full response.
For broader context, pair this with How to Build Full-Stack TypeScript Apps With AI in 2026 and The Next.js AI App Stack for 2026; those companion pieces show where this fits in the wider AI developer workflow.
async function* streamCompletion(prompt: string) {
const response = await fetch("/api/chat", {
method: "POST",
body: JSON.stringify({ prompt }),
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
yield decoder.decode(value);
}
}
// Usage
for await (const chunk of streamCompletion("Explain TypeScript generics")) {
process.stdout.write(chunk);
}
The Vercel AI SDK wraps this into streamText() which handles the protocol automatically.
2. Type-Safe Tool Definitions with Zod#
AI tools need runtime validation. Zod gives you TypeScript types and validation from a single schema.
import { z } from "zod";
import { tool } from "ai";
const weatherTool = tool({
description: "Get current weather for a location",
parameters: z.object({
city: z.string().describe("City name"),
units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
}),
execute: async ({ city, units }) => {
const data = await fetchWeather(city, units);
return { temperature: data.temp, condition: data.condition };
},
});
The parameters schema validates input AND generates the JSON Schema that the model sees. One source of truth.
3. Structured Output with Type Inference#
When you need the model to return a specific shape, not free text.
import { generateObject } from "ai";
import { z } from "zod";
const ProductReview = z.object({
sentiment: z.enum(["positive", "negative", "neutral"]),
score: z.number().min(0).max(10),
keyPoints: z.array(z.string()).max(5),
recommendation: z.boolean(),
});
type ProductReview = z.infer<typeof ProductReview>;
const { object } = await generateObject({
model: anthropic("claude-sonnet-4-6"),
schema: ProductReview,
prompt: `Analyze this review: "${reviewText}"`,
});
// object is fully typed as ProductReview
console.log(object.sentiment, object.score);
4. Retry with Exponential Backoff#
Every AI API call fails sometimes. Rate limits, timeouts, server errors. Wrap calls in retry logic.
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries) throw error;
const isRetryable =
error instanceof Error &&
(error.message.includes("429") ||
error.message.includes("503") ||
error.message.includes("timeout"));
if (!isRetryable) throw error;
const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new Error("Unreachable");
}
// Usage
const result = await withRetry(() =>
generateText({ model: anthropic("claude-sonnet-4-6"), prompt })
);
5. Discriminated Unions for Agent Actions#
When agents can take multiple action types, discriminated unions make the type system enforce correctness.
type AgentAction =
| { type: "search"; query: string }
| { type: "write_file"; path: string; content: string }
| { type: "run_command"; command: string; cwd?: string }
| { type: "ask_user"; question: string }
| { type: "done"; result: string };
function executeAction(action: AgentAction): Promise<string> {
switch (action.type) {
case "search":
return searchWeb(action.query);
case "write_file":
return writeFile(action.path, action.content);
case "run_command":
return exec(action.command, { cwd: action.cwd });
case "ask_user":
return prompt(action.question);
case "done":
return Promise.resolve(action.result);
}
}
TypeScript guarantees you handle every action type. Adding a new type without handling it is a compile error.
6. Generic Message History#
Type-safe conversation history that works across providers.
interface Message<Role extends string = string> {
role: Role;
content: string;
metadata?: Record<string, unknown>;
}
type ChatMessage = Message<"user" | "assistant" | "system">;
class Conversation {
private messages: ChatMessage[] = [];
system(content: string): this {
this.messages.push({ role: "system", content });
return this;
}
user(content: string): this {
this.messages.push({ role: "user", content });
return this;
}
assistant(content: string): this {
this.messages.push({ role: "assistant", content });
return this;
}
toArray(): ChatMessage[] {
return [...this.messages];
}
get lastAssistant(): string | undefined {
return this.messages.findLast((m) => m.role === "assistant")?.content;
}
}
7. Provider Abstraction#
Switch between AI providers without changing application code.
interface AIProvider {
generate(prompt: string, options?: GenerateOptions): Promise<string>;
stream(prompt: string, options?: GenerateOptions): AsyncIterable<string>;
}
interface GenerateOptions {
maxTokens?: number;
temperature?: number;
systemPrompt?: string;
}
function createProvider(name: "anthropic" | "openai"): AIProvider {
const providers: Record<string, AIProvider> = {
anthropic: {
generate: async (prompt, opts) => {
const { text } = await generateText({
model: anthropic("claude-sonnet-4-6"),
prompt,
maxTokens: opts?.maxTokens,
temperature: opts?.temperature,
system: opts?.systemPrompt,
});
return text;
},
stream: (prompt, opts) => streamProvider("anthropic", prompt, opts),
},
openai: {
generate: async (prompt, opts) => {
const { text } = await generateText({
model: openai("gpt-5"),
prompt,
maxTokens: opts?.maxTokens,
});
return text;
},
stream: (prompt, opts) => streamProvider("openai", prompt, opts),
},
};
return providers[name];
}
8. Token Budget Management#
Track and limit token usage per request, per user, or per session.
interface TokenBudget {
maxInput: number;
maxOutput: number;
used: { input: number; output: number };
}
function createBudget(maxInput = 100_000, maxOutput = 4_096): TokenBudget {
return { maxInput, maxOutput, used: { input: 0, output: 0 } };
}
function checkBudget(budget: TokenBudget, inputTokens: number): boolean {
return budget.used.input + inputTokens <= budget.maxInput;
}
function recordUsage(
budget: TokenBudget,
input: number,
output: number
): TokenBudget {
return {
...budget,
used: {
input: budget.used.input + input,
output: budget.used.output + output,
},
};
}
// Usage in an agent loop
let budget = createBudget();
while (checkBudget(budget, estimatedTokens)) {
const result = await generateText({ model, prompt });
budget = recordUsage(budget, result.usage.promptTokens, result.usage.completionTokens);
}
9. Type-Safe Environment Config#
Never use untyped process.env directly. Parse and validate at startup.
import { z } from "zod";
const envSchema = z.object({
ANTHROPIC_API_KEY: z.string().min(1),
OPENAI_API_KEY: z.string().min(1),
DATABASE_URL: z.string().url(),
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
MAX_TOKENS: z.coerce.number().default(4096),
ENABLE_STREAMING: z.coerce.boolean().default(true),
});
export const env = envSchema.parse(process.env);
// Now fully typed
console.log(env.ANTHROPIC_API_KEY); // string
console.log(env.MAX_TOKENS); // number
console.log(env.ENABLE_STREAMING); // boolean
Parse once at the top of your app. If any variable is missing or malformed, it crashes immediately with a clear error instead of failing silently at runtime.
10. Result Type for Error Handling#
Replace try/catch with a Result type for composable error handling.
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function ok<T>(value: T): Result<T, never> {
return { ok: true, value };
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}
async function safeGenerate(prompt: string): Promise<Result<string>> {
try {
const { text } = await generateText({
model: anthropic("claude-sonnet-4-6"),
prompt,
});
return ok(text);
} catch (e) {
return err(e instanceof Error ? e : new Error(String(e)));
}
}
// Usage - no try/catch needed
const result = await safeGenerate("Explain monads");
if (result.ok) {
console.log(result.value);
} else {
console.error("Failed:", result.error.message);
}
Frequently Asked Questions#
Which TypeScript patterns matter most for AI apps?#
Streaming (pattern 1) and structured output (pattern 3) have the biggest impact. Streaming is table stakes for user experience. Structured output eliminates parsing errors and gives you type safety on model responses.
Should I use Zod or TypeScript interfaces for AI tool parameters?#
Zod. TypeScript types disappear at runtime, but AI tools need runtime validation. Zod schemas generate both the TypeScript type (via z.infer) and the JSON Schema that models consume. One schema, two outputs.
How do I handle AI API rate limits in TypeScript?#
Use the retry with exponential backoff pattern (pattern 4). Check for 429 status codes, add jitter to prevent thundering herd, and set a max retry count. The Vercel AI SDK has built-in retry support.
What is the best way to type AI model responses?#
Use generateObject() with a Zod schema (pattern 3). The response is fully typed at compile time and validated at runtime. For streaming, use streamObject() which gives you partial typed results as they arrive.
How do I switch between Claude and GPT without rewriting code?#
Use the provider abstraction pattern (pattern 7) or the Vercel AI SDK which handles this natively. Define a common interface and swap the model string. The AI SDK supports Anthropic, OpenAI, Google, and 20+ other providers with the same API.
Get the next deep dive like this in your inbox
One email a week on TypeScript and the rest of the AI dev stack. Free.
Read next
Vercel AI SDK: Build Streaming AI Apps in TypeScript
The AI SDK is the fastest way to add streaming AI responses to your Next.js app. Here is how to use it with Claude, GPT, and open source models.
5 min readHow to Build AI Agents in TypeScript
A practical guide to building AI agents with TypeScript using the Vercel AI SDK. Tool use, multi-step reasoning, and real patterns you can ship today.
10 min readTransformers.js: Run AI Models Directly in the Browser
Transformers.js lets you run machine learning models in the browser with zero backend. Here is how to use it for text generation, speech recognition, image classification, and semantic search.
7 min readNew here? Start with
Technical content at the intersection of AI and development. Building with AI agents, Claude Code, and modern dev tools - then showing you exactly how it works.








