Agentive
ツールレビュー

TypeScript × AI開発 — 型安全なAIアプリケーション構築

約6分で読めます

AIアプリケーション開発においてTypeScriptの型システムは強力な武器になる。APIレスポンスの型定義、ストリーミング処理の型安全な実装、エラーハンドリングの網羅性チェックなど、型があることで実行時エラーを大幅に減らせる。本記事では、Anthropic SDKとVercel AI SDKを中心に、型安全なAIアプリケーションの構築手法を実践的に解説する。

Anthropic SDK for TypeScriptの基本設定

Anthropic公式のTypeScript SDKは、APIレスポンスの型定義が充実しており、型推論だけでほとんどのケースをカバーできる。

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
});

async function sendMessage(prompt: string): Promise<string> {
  const response = await client.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: 1024,
    messages: [{ role: "user", content: prompt }],
  });

  const textBlock = response.content.find((block) => block.type === "text");
  if (!textBlock || textBlock.type !== "text") {
    throw new Error("No text content in response");
  }
  return textBlock.text;
}

環境変数の型安全な管理

AI開発ではAPIキーや設定値を環境変数で管理することが多い。zodを使って環境変数のバリデーションと型付けを同時に行う。

import { z } from "zod";

const envSchema = z.object({
  ANTHROPIC_API_KEY: z.string().min(1),
  AI_MODEL: z
    .enum(["claude-sonnet-4-20250514", "claude-opus-4-20250514"])
    .default("claude-sonnet-4-20250514"),
  MAX_TOKENS: z.coerce.number().int().min(1).max(8192).default(1024),
  TEMPERATURE: z.coerce.number().min(0).max(1).default(0.7),
});

type Env = z.infer<typeof envSchema>;

構造化出力と型バリデーション

AIからの出力をアプリケーションで安全に使うには、レスポンスの構造をバリデーションする必要がある。zodスキーマを定義し、AIの出力をパースすることで型安全な構造化データを得られる。

const ProductReviewSchema = z.object({
  sentiment: z.enum(["positive", "negative", "neutral"]),
  score: z.number().min(0).max(100),
  summary: z.string().max(200),
  keyPoints: z.array(z.string()).min(1).max(5),
  recommendation: z.boolean(),
});

type ProductReview = z.infer<typeof ProductReviewSchema>;

async function analyzeReview(reviewText: string): Promise<ProductReview> {
  const response = await client.messages.create({
    model: "claude-sonnet-4-20250514",
    max_tokens: 1024,
    system: "レビューを分析してJSON形式で出力してください。",
    messages: [{ role: "user", content: reviewText }],
  });

  const text =
    response.content[0].type === "text" ? response.content[0].text : "";
  const jsonMatch = text.match(/\{[\s\S]*\}/);
  if (!jsonMatch) throw new Error("No JSON found in AI response");
  return ProductReviewSchema.parse(JSON.parse(jsonMatch[0]));
}

バリデーションエラーのリトライ戦略

AIの出力がスキーマに合致しない場合、エラー内容をフィードバックしてリトライする仕組みを実装する。

async function analyzeWithRetry(
  reviewText: string,
  maxRetries: number = 3
): Promise<ProductReview> {
  let lastError: Error | null = null;
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await analyzeReview(reviewText);
    } catch (error) {
      lastError = error as Error;
      console.warn("Validation failed on attempt", i + 1);
    }
  }
  throw lastError || new Error("All retries failed");
}

ストリーミングレスポンスの型安全な実装

チャットUIなどではストリーミング出力が必須になる。Anthropic SDKのストリーミングAPIは型安全なイベントハンドリングを提供している。

async function streamResponse(
  prompt: string,
  onChunk: (text: string) => void,
  onComplete: (fullText: string, usage: {
    inputTokens: number;
    outputTokens: number;
  }) => void
): Promise<void> {
  const stream = await client.messages.stream({
    model: "claude-sonnet-4-20250514",
    max_tokens: 2048,
    messages: [{ role: "user", content: prompt }],
  });

  let fullText = "";

  for await (const event of stream) {
    if (
      event.type === "content_block_delta" &&
      event.delta.type === "text_delta"
    ) {
      fullText += event.delta.text;
      onChunk(event.delta.text);
    }
  }

  const finalMessage = await stream.finalMessage();
  onComplete(fullText, {
    inputTokens: finalMessage.usage.input_tokens,
    outputTokens: finalMessage.usage.output_tokens,
  });
}

Vercel AI SDKとの統合

Vercel AI SDKは、複数のAIプロバイダーを統一的なインターフェースで扱える抽象化レイヤーである。Next.jsとの統合が特に強力で、Server ActionsやRoute Handlersとシームレスに連携できる。

// app/api/chat/route.ts (Next.js App Router)
import { anthropic } from "@ai-sdk/anthropic";
import { streamText } from "ai";

export async function POST(req: Request) {
  const { messages } = await req.json();
  const result = streamText({
    model: anthropic("claude-sonnet-4-20250514"),
    system: "あなたは親切なアシスタントです。日本語で回答してください。",
    messages,
    maxTokens: 2048,
  });
  return result.toDataStreamResponse();
}

クライアント側の実装

"use client";
import { useChat } from "@ai-sdk/react";

export default function ChatPage() {
  const { messages, input, handleInputChange, handleSubmit, isLoading } =
    useChat({ api: "/api/chat" });

  return (
    <div className="max-w-2xl mx-auto p-4">
      <div className="space-y-4">
        {messages.map((m) => (
          <div key={m.id} className="p-3 rounded-lg">{m.content}</div>
        ))}
      </div>
      <form onSubmit={handleSubmit} className="flex gap-2 pt-4">
        <input value={input} onChange={handleInputChange}
          className="flex-1 border rounded-lg px-4 py-2" />
        <button type="submit" disabled={isLoading}
          className="bg-blue-500 text-white px-4 py-2 rounded-lg">送信</button>
      </form>
    </div>
  );
}

エラーハンドリングのパターン

AI API呼び出しでは、レート制限、タイムアウト、無効なレスポンスなど、様々なエラーが発生しうる。TypeScriptのDiscriminated Unionを使って、エラーを型安全に分類・処理する。

type AIError =
  | { type: "rate_limit"; retryAfter: number }
  | { type: "timeout"; elapsedMs: number }
  | { type: "auth_error"; message: string }
  | { type: "unknown"; error: Error };

function classifyError(error: unknown): AIError {
  if (error instanceof Anthropic.RateLimitError) {
    return { type: "rate_limit", retryAfter: 60 };
  }
  if (error instanceof Anthropic.AuthenticationError) {
    return { type: "auth_error", message: error.message };
  }
  return { type: "unknown", error: error as Error };
}

async function handleAICall<T>(fn: () => Promise<T>): Promise<T> {
  try {
    return await fn();
  } catch (error) {
    const classified = classifyError(error);
    switch (classified.type) {
      case "rate_limit":
        await new Promise((r) => setTimeout(r, classified.retryAfter * 1000));
        return handleAICall(fn);
      case "auth_error":
        throw new Error("APIキーを確認してください");
      default:
        throw classified.type === "unknown" ? classified.error : new Error(classified.type);
    }
  }
}

Tool Use(Function Calling)の型定義

Claude APIのTool Use機能をTypeScriptで型安全に実装することで、AIが外部関数を呼び出すエージェントパターンを構築できる。

interface WeatherTool {
  name: "get_weather";
  input: { city: string; unit?: "celsius" | "fahrenheit" };
}

interface SearchTool {
  name: "search_web";
  input: { query: string; limit?: number };
}

type ToolCall = WeatherTool | SearchTool;

async function executeTool(tool: ToolCall): Promise<string> {
  switch (tool.name) {
    case "get_weather":
      return "Weather data for " + tool.input.city;
    case "search_web":
      return "Search results for " + tool.input.query;
    default:
      const _exhaustive: never = tool;
      throw new Error("Unknown tool");
  }
}

比較: 主要AI SDK

特徴Anthropic SDKVercel AI SDKLangChain.js
型安全性高(公式型定義)高(Zodスキーマ統合)中(ジェネリクスベース)
ストリーミングネイティブ対応React Hooks統合アダプター経由
マルチプロバイダーClaude専用複数対応複数対応
バンドルサイズ軽量中程度大きい
学習コスト低い中程度高い

関連記事

A

Agentive 編集部

AIエージェントを実際に使い倒す個人開発者。サイト制作の自動化を実践しながら、その知見を発信しています。