P020 min

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.

SDK Focusasync/awaitfor await..ofZodstructured_output

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..of for 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.

typescript
// @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 await must be marked async
  • await pauses execution until the Promise resolves
  • Error handling uses standard try/catch blocks
typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
// @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:

typescript
// 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:

typescript
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:

typescript
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
typescript
// 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:

  1. Define an interface for your agent's configuration
  2. Write an async function that takes typed parameters
  3. Use for await..of to process the response stream
  4. 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:

typescript
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 async function that uses await
  • 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 interface for 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.

Sign in to save lesson progress, unlock flashcards, and continue where you left off.