こんにちは😊
株式会社プロドウガの@YushiYamamotoです!
らくらくサイトの開発・運営を担当しながら、React.js・Next.js専門のフリーランスエンジニアとしても活動しています❗️
この記事は TypeScript Advent Calendar 2025 の参加記事です🎄
2025年現在、Web開発において「生成AI(LLM)の組み込み」は当たり前になりました。
しかし、TypeScript使いの私たちには一つの悩みがあります。
「AIが返してくるJSON、本当にその型であってる?」
JSON.parse() した結果を as MyType でキャストして安心していませんか?
AIは平気で嘘をつきますし、たまにJSONの構造を間違えます。ランタイムで型が合わずにアプリが落ちる……そんな悪夢を防ぐための、TypeScript × Zod × AI の鉄板パターンをご紹介します。
😨 as キャストの危険性
AIを使って「料理のレシピ」を生成する機能を考えてみましょう。
私たちは以下のような型を期待しています。
type Recipe = {
title: string;
ingredients: string[];
cookingTime: number;
};
しかし、OpenAI APIから返ってくるのはただの string です。
これをこう書いてしまうのは、非常に危険です。
// 💀 危険なコード
const response = await openai.chat.completions.create({ ... });
const jsonString = response.choices[0].message.content;
// ここでAIが { "name": "カレー" } みたいな違うキーを返してきたら爆発する
const recipe = JSON.parse(jsonString) as Recipe;
TypeScriptはコンパイル時にはエラーを出しませんが、実行時に recipe.title にアクセスした瞬間、undefined になりバグを生みます。
🛡️ Zodで「実行時バリデーション」を行う
そこで登場するのが、スキーマバリデーションライブラリの Zod です。
Zodを使えば、「TypeScriptの型定義」と「実行時のチェックロジック」を同時に定義できます。
ステップ1:スキーマの定義
まずはZodでスキーマを定義し、そこからTypeScriptの型を抽出します。
これで「型定義」と「バリデーションルール」の二重管理がなくなります。
import { z } from "zod";
// Zodスキーマの定義
const RecipeSchema = z.object({
title: z.string().describe("料理のタイトル"),
ingredients: z.array(z.string()).describe("材料のリスト"),
cookingTime: z.number().int().min(1).describe("調理時間(分)"),
difficulty: z.enum(["easy", "medium", "hard"]).describe("難易度"),
});
// スキーマからTypeScriptの型を自動生成
type Recipe = z.infer<typeof RecipeSchema>;
describe() が重要!
Zodの .describe() に書いた説明文は、後述するOpenAIのStructured Outputs機能を使う際に、AIへのヒントとして渡されます。「型」自体がプロンプトになるのです。
🤖 OpenAI Structured Outputs との連携
OpenAIの response_format にZodスキーマを渡すことで、AIに対して「このJSONスキーマ以外は絶対に返さないでくれ」と強制できます。
(※ zod-response-format ヘルパーなどを使用する想定)
実装コード例
import OpenAI from "openai";
import { z } from "zod";
import { zodResponseFormat } from "openai/helpers/zod";
const openai = new OpenAI();
// スキーマ定義(前述と同じ)
const RecipeSchema = z.object({
title: z.string(),
ingredients: z.array(z.string()),
cookingTime: z.number(),
difficulty: z.enum(["easy", "medium", "hard"]),
});
async function generateRecipe(keyword: string) {
const completion = await openai.chat.completions.create({
model: "gpt-4o-2024-08-06", // Structured Outputs対応モデル
messages: [
{ role: "system", content: "あなたはプロの料理人です。" },
{ role: "user", content: `${keyword}を使ったレシピを考えて。` },
],
// ここでZodスキーマを渡す!
response_format: zodResponseFormat(RecipeSchema, "recipe"),
});
const content = completion.choices[0].message.content;
// 念のためZodでパースする(二重の防御)
// AIが万が一フォーマットを破っても、ここで検知して安全にエラーハンドリングできる
const result = RecipeSchema.safeParse(JSON.parse(content || "{}"));
if (!result.success) {
console.error("AIが不正なJSONを返しました", result.error);
throw new Error("レシピ生成に失敗しました");
}
// ここでは result.data は確実に Recipe 型であることが保証される
return result.data;
}
💡 なぜこの構成が最強なのか?
-
型安全性の保証:
result.dataはランタイムでも確実に型と一致しています。undefinedアクセスによるクラッシュを防げます。 - プロンプト管理の効率化: Zodスキーマを書くだけで、それがそのままAIへの「出力指示書」になります。プロンプトで「JSONで返して。キーはtitleで...」と長々と書く必要がなくなります。
- 開発体験(DX)の向上: VS Codeなどのエディタで、AIのレスポンスに対して完璧な補完が効きます。
まとめ
TypeScriptを使う最大のメリットは「安心感」です。
しかし、外部API(特にAI)との境界線では、その安心感が崩れがちです。
「AIを信じるな、Zod(スキーマ)を信じろ」
これを合言葉に、2025年も堅牢なTypeScriptライフを送りましょう!🚀
最後に:業務委託のご相談を承ります
私は業務委託エンジニアとしてWEB制作やシステム開発を請け負っています。最新技術を活用したレスポンシブなWebサイト制作、インタラクティブなアプリケーション開発、API連携など幅広いご要望に対応可能です。
「課題解決に向けた即戦力が欲しい」「高品質なWeb制作を依頼したい」という方は、お気軽にご相談ください。一緒にビジネスの成長を目指しましょう!
👉 ポートフォリオ
🌳 らくらくサイト