Zodスキーマ vs TypeScriptインターフェース:それぞれの使い分け
ZodランタイムバリデーションスキーマとTypeScriptコンパイル時インターフェースを比較。それぞれの使い分けと、最大限の型安全性を得るための組み合わせ方を学びます。
Advanced
詳細な説明
ランタイム vs コンパイル時の型安全性
TypeScriptインターフェースとZodスキーマは異なる目的を果たします。それぞれをいつ使うか、そしてどう組み合わせるかを理解することが、堅牢なアプリケーション構築の鍵です。
TypeScriptインターフェース(コンパイル時のみ)
interface User {
id: number;
name: string;
email: string;
}
// TypeScriptはコンパイル時にあなたを信頼します
const user: User = JSON.parse(apiResponse);
// ランタイムバリデーションなし!APIが不正なデータを返してもTypeScriptは検出しません。
Zodスキーマ(ランタイム + コンパイル時)
import { z } from "zod";
const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof userSchema>;
// ランタイムバリデーションが不正なデータをキャッチ
const user = userSchema.parse(JSON.parse(apiResponse));
// データが一致しない場合ZodErrorをスロー
比較
| 特徴 | TypeScriptインターフェース | Zodスキーマ |
|---|---|---|
| コンパイル時の型 | はい | はい(z.infer経由) |
| ランタイムバリデーション | なし | あり |
| カスタムバリデーション | なし | あり(.email()、.min()、.refine()) |
| デフォルト値 | なし | あり(.default()) |
| データ変換 | なし | あり(.transform()) |
| エラーメッセージ | コンパイルエラーのみ | 詳細なランタイムエラー |
| バンドルサイズ影響 | なし(消去される) | ~13KB(minified + gzipped) |
| 学習曲線 | 低い | 中程度 |
TypeScriptのみを使うべき場合
- 内部データ — 信頼境界を越えない状態(コンポーネントprops、関数の戻り値)。
- パフォーマンスクリティカルなホットパス — ~1msのパースオーバーヘッドが重要な場合。
- シンプルなアプリケーション — すべてのデータソースを完全に信頼できる場合。
Zodを使うべき場合
- API境界 — 外部APIからのレスポンスの検証。
- ユーザー入力 — フォームデータ、クエリパラメータ、ファイルアップロード。
- 環境変数 — 起動時の設定パース。
- データベース結果 — ORM出力が期待に一致することの確認。
- Webhookペイロード — 外部システムのコールバック。
ベストプラクティス:両方を組み合わせる
// スキーマを一度定義し、そこから型を派生
const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
// 型は派生される — 重複なし
type User = z.infer<typeof userSchema>;
// 内部コードには型を使用
function formatUser(user: User): string {
return \`\${user.name} <\${user.email}>\`;
}
// 信頼境界ではスキーマを使用
async function fetchUser(id: number): Promise<User> {
const res = await fetch(\`/api/users/\${id}\`);
return userSchema.parse(await res.json());
}
このパターンにより、内部コードではゼロオーバーヘッドの型チェック、境界ではランタイムの安全性という両方の利点が得られます。
ユースケース
TypeScriptプロジェクトにZodを追加するかどうかを評価し、コンパイル時のみの型とランタイムバリデーションスキーマのトレードオフを理解する必要がある場合に使用します。