TypeScript Fundamentals
Master the TypeScript patterns essential for agent development: async operations, streaming responses, type-safe schemas, and interfaces that power the Claude Agent SDK.
Why TypeScript for Agents?
Agent development requires handling asynchronous operations, streaming data, and structured outputs. TypeScript provides:
- Type Safety - Catch errors at compile time, not runtime
- Async/Await - Clean syntax for handling LLM responses and tool calls
- Streaming Support - Native
for await..offor processing agent messages - Schema Validation - Zod schemas ensure structured output matches expectations
The Claude Agent SDK is built in TypeScript and leverages these features throughout its API.
Pattern 1: Async/Await Fundamentals
Agent operations are inherently asynchronous — LLM calls, tool execution, and file I/O all take time. TypeScript's async/await provides a synchronous-looking syntax for async operations.
// @lesson-illustrative
// Generic illustration of async/await syntax — `apiCall` and
// `processResponse` here are placeholder names, not the SDK's `query()`.
// (See Pattern 2 below for the real Agent SDK shape.)
// Without async/await (callback hell)
function oldStyle() {
apiCall(prompt).then((response) => {
processResponse(response).then((result) => {
console.log(result);
});
});
}
// With async/await (clean and readable)
async function modernStyle() {
const response = await apiCall(prompt);
const result = await processResponse(response);
console.log(result);
}Key Rules:
- Functions that use
awaitmust be markedasync awaitpauses execution until the Promise resolves- Error handling uses standard
try/catchblocks
import { query } from "@anthropic-ai/claude-agent-sdk";
async function runAgent() {
try {
const response = query({
prompt: "What is 2+2?",
options: { model: "sonnet", tools: [] },
});
// Process streaming response
for await (const message of response) {
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}
} catch (error) {
console.error("Agent failed:", error);
}
}SDK Insight: Query Returns AsyncIterable
The Claude Agent SDK's query() function returns an AsyncIterable, not a Promise. This enables
streaming responses instead of waiting for the entire output to complete.
Pattern 2: Streaming with for await..of
Agents generate responses incrementally. The for await..of loop processes each message as it arrives:
import { query, type SDKAssistantMessage } from "@anthropic-ai/claude-agent-sdk";
function getAssistantText(message: SDKAssistantMessage): string {
return message.message.content
.filter((block): block is Extract<typeof block, { type: "text" }> => block.type === "text")
.map((block) => block.text)
.join("\n");
}
const response = query({
prompt: "List files and calculate their total size",
options: { tools: ["Bash", "Read"], allowedTools: ["Read"] },
});
// Process each message in real-time
for await (const message of response) {
switch (message.type) {
case "system":
console.log(`Session: ${message.session_id}`);
break;
case "assistant":
console.log(`Assistant: ${getAssistantText(message)}`);
break;
case "tool_use_summary":
console.log(`Using tool: ${message.summary}`);
break;
case "result":
if (message.subtype === "success") {
console.log(`Final: ${message.result}`);
}
break;
}
}Why Streaming Matters:
- UX - Show progress instead of blank screens
- Debugging - Observe visible messages and tool summaries in real time
- Control - Cancel long-running operations early
- Memory - Process large outputs incrementally
SDK Insight: Message Types
In beginner loops, the most important message types are system, assistant, tool_use_summary,
and result. Type discrimination via message.type stays type-safe because the SDK exposes a
discriminated union over the full stream surface.
Pattern 3: Zod Schemas for Structured Output
Agents generate unstructured text by default. Zod schemas constrain output to match your data structures:
import { z } from "zod";
import { query } from "@anthropic-ai/claude-agent-sdk";
// Define expected output structure
const TaskSchema = z.object({
title: z.string(),
priority: z.enum(["low", "medium", "high"]),
subtasks: z.array(z.string()),
estimatedHours: z.number().positive(),
});
type Task = z.infer<typeof TaskSchema>;
const response = query({
prompt: "Break down 'Build a chat app' into subtasks",
options: {
outputFormat: {
type: "json_schema",
schema: z.toJSONSchema(TaskSchema) as Record<string, unknown>,
},
},
});
for await (const message of response) {
if (
message.type === "result" &&
message.subtype === "success" &&
message.structured_output !== undefined
) {
const task: Task = TaskSchema.parse(message.structured_output);
console.log(`Task: ${task.title}`);
console.log(`Priority: ${task.priority}`);
console.log(`Subtasks: ${task.subtasks.length}`);
}
}Zod Schema Benefits:
- Validation - Throws error if LLM output doesn't match schema
- Type Inference -
z.infer<typeof Schema>generates TypeScript types - Documentation - Schema serves as both runtime validator and compile-time type
Try ItSchema Refinement
Add .refine() to enforce a real approval rule. Decide who consumes the result and what a false rejection costs before choosing the boundary:
const BudgetSchema = z
.object({
amount: z.number(),
currency: z.string(),
})
.refine((data) => data.amount > 0 && data.amount < 1000000, {
message: "Budget must be between $0 and $1M",
});Test valid, invalid, and boundary values; explain whether the message helps the user recover.
Pattern 4: Type Annotations Basics
TypeScript type annotations prevent bugs by catching type mismatches at compile time:
Without types, the bug only surfaces at runtime:
// @lesson-illustrative
// Untyped — this compiles, but `content.substring` blows up if a caller
// passes a number or null. TypeScript can't help here.
function summarize(content) {
return content.substring(0, 100);
}Annotating the parameter shifts the failure to compile time:
// Typed — TypeScript rejects any caller that passes a non-string.
function summarize(content: string): string {
return content.substring(0, 100);
}Common Type Annotations in Agent Code:
import { query, type Options, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";
// Function parameters and return types
async function runAgent(prompt: string, tools: string[]): Promise<string> {
const options: Options = { tools, allowedTools: tools };
const response = query({ prompt, options });
let finalContent: string = "";
for await (const message of response) {
if (message.type === "result" && message.subtype === "success") {
finalContent = message.result;
}
}
return finalContent;
}
// Variable type inference (TypeScript infers the type)
const config: Options = {
model: "sonnet",
maxTurns: 10,
};
// Explicit type annotation (when inference needs help)
const messages: SDKMessage[] = [];Type Inference vs Explicit Types
TypeScript infers types in most cases. Explicit annotations are needed when: 1. Function parameters (no inference) 2. Empty arrays/objects (ambiguous) 3. Complex return types (clarity)
Pattern 5: Interfaces for Agent Data
Interfaces define contracts for agent data structures. They're especially useful for tool results and agent state:
import { z } from "zod";
import { query } from "@anthropic-ai/claude-agent-sdk";
// Tool result interface
interface FileAnalysis {
path: string;
sizeBytes: number;
lineCount: number;
language: string;
}
// Agent state interface
interface AgentSession {
sessionId: string;
turnCount: number;
toolCallsUsed: string[];
memorySlots: Map<string, unknown>;
}
// Using interfaces with the SDK
async function analyzeFile(filePath: string): Promise<FileAnalysis> {
const response = query({
prompt: `Analyze ${filePath}`,
options: {
tools: ["Read", "Bash"],
allowedTools: ["Read"],
outputFormat: {
type: "json_schema",
schema: z.toJSONSchema(
z.object({
path: z.string(),
sizeBytes: z.number(),
lineCount: z.number(),
language: z.string(),
})
) as Record<string, unknown>,
},
},
});
for await (const message of response) {
if (
message.type === "result" &&
message.subtype === "success" &&
message.structured_output !== undefined
) {
return z
.object({
path: z.string(),
sizeBytes: z.number(),
lineCount: z.number(),
language: z.string(),
})
.parse(message.structured_output);
}
}
throw new Error("No result received");
}Interfaces vs Types:
- Interface - Extendable, better for object shapes, clearer errors
- Type - Union types, utility types, aliases
// Interface (preferred for objects)
interface AgentConfig {
model: string;
maxTurns: number;
}
// Type (preferred for unions)
type MessageType = SDKMessage["type"];
// Extending interfaces
interface AdvancedAgentConfig extends AgentConfig {
temperature: number;
topP: number;
}Try ItType Your First Agent
Choose a small team workflow and create its agent with full type annotations:
- Define an
interfacefor your agent's configuration - Write an
async functionthat takes typed parameters - Use
for await..ofto process the response stream - Add a Zod schema for structured output
Name the downstream user and one malformed output that TypeScript or Zod must stop before runtime.
Putting It All Together
Here's a complete example combining all 5 patterns:
import { z } from "zod";
import { query } from "@anthropic-ai/claude-agent-sdk";
// Pattern 5: Interface for configuration
interface CodeReviewConfig {
filePath: string;
focusAreas: string[];
maxIssues: number;
}
// Pattern 3: Zod schema for output
const IssueSchema = z.object({
severity: z.enum(["low", "medium", "high", "critical"]),
line: z.number(),
description: z.string(),
suggestion: z.string(),
});
const ReviewSchema = z.object({
summary: z.string(),
issues: z.array(IssueSchema),
rating: z.number().min(0).max(10),
});
type CodeReview = z.infer<typeof ReviewSchema>;
// Pattern 1 & 4: Async function with type annotations
async function reviewCode(config: CodeReviewConfig): Promise<CodeReview> {
const response = query({
prompt: `Review ${config.filePath} focusing on: ${config.focusAreas.join(", ")}`,
options: {
model: "sonnet",
tools: ["Read", "Grep"],
allowedTools: ["Read", "Grep"],
outputFormat: {
type: "json_schema",
schema: z.toJSONSchema(ReviewSchema) as Record<string, unknown>,
},
},
});
// Pattern 2: Streaming with for await..of
for await (const message of response) {
if (message.type === "tool_use_summary") {
console.log(`Reading: ${message.summary}`);
}
if (
message.type === "result" &&
message.subtype === "success" &&
message.structured_output !== undefined
) {
return ReviewSchema.parse(message.structured_output);
}
}
throw new Error("Review failed");
}
// Usage
const review = await reviewCode({
filePath: "src/agent.ts",
focusAreas: ["security", "performance"],
maxIssues: 10,
});
console.log(`Rating: ${review.rating}/10`);
console.log(`Issues found: ${review.issues.length}`);Before proceeding to P1 Agent Basics, verify you can:
- Write an
asyncfunction that usesawait - Process agent responses with
for await..of - Define a Zod schema and use
z.infer<typeof Schema> - Add type annotations to function parameters
- Create an
interfacefor structured data
These patterns appear in every Claude Agent SDK example — mastering them now will accelerate your learning in subsequent paths.
Going Further
As you progress through the course, you'll encounter advanced TypeScript patterns like branded types, discriminated unions for message handling, and strict null checks. These build naturally on the 5 patterns above.
Related Reading
With these TypeScript fundamentals in place, you're ready to:
- P1 · Agent Basics - Build your first agent loop with full type safety
- P2 · Tool Use - Define typed tool schemas and result handlers
- P3 · Prompts - Use Zod schemas for structured prompt engineering
- P4 · Memory - Type session state and memory management
The Claude Agent SDK assumes TypeScript proficiency — these 5 patterns form the foundation for everything that follows.