多くのTypeScript開発者が陥りがちな落とし穴、それは**「コンパイル時の型チェックだけではランタイムエラーを防ぎきれない」**という事実です。APIからのレスポンスやユーザーからのフォーム入力など、外部からやってくるデータは、あなたがTypeScriptで厳密に型定義したはずの構造と異なることがあります。これを知らないと、本番環境で予期せぬエラーが発生し、アプリケーションがクラッシュする原因にもなりかねません。
この記事では、そんなランタイムエラーからアプリケーションを保護するための強力なツール、ZodとValibotという2つの人気バリデーションライブラリに焦点を当てます。具体的な実装例を通して、安全なAPI連携やフォーム入力のランタイムバリデーションを実践する方法、それぞれのメリット・デメリット、そしてプロジェクトに応じた選定基準を解説し、あなたのTypeScriptプロジェクトの堅牢性を高めるための実践的な知見を提供します。
TypeScriptプロジェクトにおけるランタイムバリデーションの必要性
TypeScriptは強力な静的型付け言語ですが、その型チェックはあくまでコンパイル時に行われます。しかし、Webアプリケーションでは、外部APIからのレスポンス、データベースからのデータ、ユーザーからのフォーム入力など、ランタイムに初めてその構造が確定するデータが多く存在します。これらのデータがTypeScriptで定義した型と一致しない場合、コンパイル時には問題がなくても、実行時に予期せぬエラー(TypeErrorなど)が発生する可能性があります。
例えば、User型を定義していても、APIが意図せずnameプロパティを省略したり、idを数値ではなく文字列で返したりする可能性はゼロではありません。このギャップを埋めるのがランタイムバリデーションです。ZodやValibotのようなライブラリは、実行時にデータの構造と型を検証し、定義されたスキーマに合致しないデータを発見した場合にエラーを通知したり、安全な型に変換したりすることで、アプリケーションの堅牢性を大幅に向上させます。
ZodとValibot:最新の技術情報と特徴
まずは、ZodとValibotの基本的な情報と特徴を把握しましょう。
Zod (zod@3.23.8) の特徴
Zodは「TypeScriptファースト」を謳うスキーマ宣言・バリデーションライブラリで、その直感的なAPIと強力な型推論機能が特徴です。
-
バージョン: 執筆時点での最新安定版は
zod@3.23.8です。(Node.js環境でnpm view zod versionで確認できます) - 特徴: スキーマ定義からTypeScriptの型を自動生成し、コンパイル時とランタイムの型整合性を保証します。メソッドチェーンによる簡潔な構文で、複雑なバリデーションルールも記述しやすいです。
-
API:
z.object(),z.string(),z.number(),z.email(),z.url(),z.min(),z.max()などの豊富なメソッドを提供します。z.infer<typeof Schema>を使用してスキーマからTypeScriptの型を自動生成できます。 -
エラーハンドリング: バリデーション失敗時には
ZodErrorインスタンスをスローします。safeParse()メソッドを使用すると、エラーをスローせずに結果をオブジェクト形式で受け取ることができます。 -
主な強化点 (v3.x): バンドルサイズの削減、文字列パースの高速化、
z.toJSONSchema()によるJSON Schemaへの変換、ファイルスキーマ、i18n対応などが強化されています。Zod Miniという、よりツリーシェイク可能なバージョンも提供されています。
Valibot (v0.30.0) の特徴
Valibotは、モジュール式で型安全、そして極めて軽量なスキーマライブラリとして注目を集めています。
-
バージョン: 執筆時点での最新安定版は
valibot@0.30.0です。(Node.js環境でnpm view valibot versionで確認できます) - 特徴: バンドルサイズの小ささと依存関係ゼロが最大の特徴です。型安全性を重視し、Zodとは異なる関数型スタイルのAPIを採用しています。
-
API:
v.object(),v.string(),v.number(),v.email(),v.minLength(),v.pipe()などの関数を提供します。Zodのようなメソッドチェーンではなく、小さく独立した関数をpipeで組み合わせてスキーマを構築します。v.InferOutput<typeof Schema>を使用してスキーマからTypeScriptの型を推論できます。 -
エラーハンドリング:
v.parse()はバリデーション失敗時にValiErrorをスローし、v.safeParse()はエラーをスローせずに結果をオブジェクト形式で返します。v.message()メソッドでカスタムエラーメッセージを定義できます。 -
主な強化点 (v0.30.0):
messageメソッドによるカスタムエラーメッセージの簡素化、summarizeメソッドによるバリデーションエラーの要約機能などが追加されています。型強制のための強力な変換アクション、AIツール統合を改善する新しいメタデータ機能、ISBNバリデーション、型の絞り込みと真偽値のパースのための新しいパイプラインツール、スキーマ結果のキャッシュ、ドメイン、JWSコンパクト、ISRCチェックによる文字列バリデーションの拡張などが含まれます。
実装の具体例:APIレスポンスのバリデーション
ここでは、実際にZodとValibotを使ってAPIレスポンスをバリデーションする例を見ていきましょう。
ZodでのAPIレスポンスバリデーション
Zodを使ったAPIレスポンスのランタイムバリデーションは、直感的な構文で記述できます。
まず、Zodをインストールします。
npm install zod
# または yarn add zod
次に、以下のコードでユーザーデータのスキーマを定義し、APIからのレスポンスを検証します。
import { z } from "zod";
// ユーザーデータのスキーマ定義
// 各プロパティの型とバリデーションルールを定義します。
const UserSchema = z.object({
id: z.number().int().positive("IDは正の整数である必要があります"), // 整数かつ正の数
name: z.string().min(1, "名前は必須です"), // 1文字以上の文字列
email: z.string().email("無効なメールアドレス形式です"), // メールアドレス形式
profile_url: z.string().url("無効なURL形式です").optional(), // オプション項目でURL形式
});
// スキーマからTypeScriptの型を自動生成
// これにより、ランタイムバリデーションとコンパイル時の型チェックが連携します。
type User = z.infer<typeof UserSchema>;
async function fetchUser(userId: number): Promise<User | null> {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
console.error(`APIエラー: ${response.status}`);
return null;
}
const data: unknown = await response.json(); // APIレスポンスは未知の型として受け取る
// データのバリデーションとパース
// ここでランタイムバリデーションが実行され、不正なデータであれば例外がスローされます。
const user = UserSchema.parse(data);
console.log("取得したユーザーデータ:", user);
return user;
} catch (error) {
if (error instanceof z.ZodError) {
// ZodErrorの場合、詳細なバリデーションエラー情報を取得できます。
console.error("バリデーションエラー:", error.errors);
console.error("バリデーションエラー (整形済み):", error.flatten()); // よりユーザーフレンドリーな形式
} else {
console.error("予期せぬエラー:", error);
}
return null;
}
}
// 使用例
fetchUser(1).then(user => {
if (user) {
console.log(`ユーザー名: ${user.name}`);
}
});
// 不正なデータでのバリデーション例
const invalidUserData = {
id: "abc", // numberではなくstring
name: "", // 最小文字数違反
email: "invalid-email", // メールアドレス形式違反
};
try {
UserSchema.parse(invalidUserData);
} catch (error) {
if (error instanceof z.ZodError) {
console.error("不正なデータでのバリデーションエラー:", error.errors);
}
}
// safeParse を使用したエラーハンドリング例
// 例外をスローせず、結果オブジェクトで成功/失敗を判定できます。
const safeParseResult = UserSchema.safeParse(invalidUserData);
if (safeParseResult.success) {
console.log("safeParse成功:", safeParseResult.data);
} else {
console.error("safeParse失敗:", safeParseResult.error.flatten());
}
ValibotでのAPIレスポンスバリデーション
Valibotも同様にAPIレスポンスのランタイムバリデーションに利用できます。Valibotは関数型のAPIが特徴です。
まず、Valibotをインストールします。
npm install valibot
# または yarn add valibot
次に、以下のコードでユーザーデータのスキーマを定義し、APIからのレスポンスを検証します。
import * as v from "valibot";
// ユーザーデータのスキーマ定義
// v.pipe() を使って複数のバリデーションルールを組み合わせます。
const UserSchema = v.object({
id: v.pipe(v.number(), v.integer("IDは整数である必要があります"), v.minValue(1, "IDは正の整数である必要があります")),
name: v.pipe(v.string(), v.minLength(1, "名前は必須です")),
email: v.pipe(v.string(), v.email("無効なメールアドレス形式です")),
profile_url: v.optional(v.pipe(v.string(), v.url("無効なURL形式です"))), // オプション項目
});
// スキーマからTypeScriptの型を自動生成
// Zodと同様に、型安全性を確保します。
type User = v.InferOutput<typeof UserSchema>;
async function fetchUserValibot(userId: number): Promise<User | null> {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
console.error(`APIエラー: ${response.status}`);
return null;
}
const data: unknown = await response.json();
// データのバリデーションとパース
// Valibotのparse関数はスキーマとデータを引数に取ります。
const user = v.parse(UserSchema, data);
console.log("取得したユーザーデータ (Valibot):", user);
return user;
} catch (error) {
if (error instanceof v.ValiError) { // ValibotのエラーはValiError
console.error("バリデーションエラー (Valibot):", error.issues);
console.error("バリデーションエラー (Valibot, 整形済み):", v.summarize(error)); // エラーの要約
} else {
console.error("予期せぬエラー (Valibot):", error);
}
return null;
}
}
// 使用例
fetchUserValibot(2).then(user => {
if (user) {
console.log(`Valibot ユーザー名: ${user.name}`);
}
});
// 不正なデータでのバリデーション例
const invalidUserDataValibot = {
id: "xyz", // numberではなくstring
name: "", // 最小文字数違反
email: "bad-email", // メールアドレス形式違反
};
try {
v.parse(UserSchema, invalidUserDataValibot);
} catch (error) {
if (error instanceof v.ValiError) {
console.error("不正なデータでのバリデーションエラー (Valibot):", error.issues);
}
}
// safeParse を使用したエラーハンドリング例
const safeParseResultValibot = v.safeParse(UserSchema, invalidUserDataValibot);
if (safeParseResultValibot.success) {
console.log("safeParse成功 (Valibot):", safeParseResultValibot.output);
} else {
console.error("safeParse失敗 (Valibot):", v.summarize(safeParseResultValibot.error));
}
よくあるエラー・ハマりどころと回避策
ZodやValibotを導入する際によくある問題とその解決策を理解しておくことで、スムーズな開発が可能です。
1. TypeScriptの型とランタイムデータの不一致
-
ハマりどころ: TypeScriptの型定義はコンパイル時にのみ有効で、ランタイムではJavaScriptに変換されるため消滅します。このため、APIレスポンスなどの外部データは、定義した型と異なる可能性があります。
as Typeのような型アサーションを安易に使うと、コンパイルエラーは出なくても、実行時にデータ構造の不一致によるエラーが発生しやすくなります。 -
回避策: ZodやValibotのようなランタイムバリデーションライブラリを導入し、外部から来るデータを必ず検証します。スキーマからTypeScriptの型を生成することで、型定義の重複を防ぎ、コンパイル時とランタイムでの整合性を保証できます。
parse()メソッドはバリデーション失敗時に例外をスローするため、try-catchブロックで囲むか、safeParse()メソッドを使用してエラーをオブジェクトとして処理するのが安全です。
2. Zodのエラーメッセージのカスタマイズと国際化 (i18n)
- ハマりどころ: デフォルトのエラーメッセージは、ユーザーにとって分かりにくい場合があります。また、多言語対応が必要なアプリケーションでは、エラーメッセージの国際化 (i18n) が課題となります。
-
回避策: Zodでは、スキーマ定義時に
messageオプションでカスタムエラーメッセージを指定できます(例:z.string().min(1, "名前は必須です"))。より大規模なi18n対応には、z.setErrorMap()を使用してグローバルなエラーマップを設定することで対応可能です。Valibotでもv.message()メソッドでカスタムメッセージを設定できます。
3. ValibotのAPI変更による混乱
- ハマりどころ: Valibotは比較的新しいライブラリであり、特にv0.31.0でAPIが大幅に変更されました。古い記事やドキュメントを参照すると、現在のv0.30.0系とは異なる構文で書かれたコードに出くわし、型エラーなどで動かないことがあります。
-
回避策: Valibotの公式ドキュメント(特にAPIリファレンスやリリースノート)を参照し、常に最新のAPI仕様に準拠したコードを書くようにします。特に
pipe構文の理解が重要です。本記事では執筆時点の最新安定版であるv0.30.0系の構文を使用しています。
設計上のトレードオフとベストプラクティス
ZodとValibotはどちらも優れたランタイムバリデーションライブラリですが、それぞれに得意な領域や設計思想の違いがあります。プロジェクトの特性に合わせて最適な選択をすることが重要です。
バンドルサイズ vs. 開発体験 (DX) / エコシステム
-
Valibot: モジュール設計により、バンドルサイズが非常に小さいという明確な利点があります(Zod v3.xと比較しても小さい傾向)。これは、エッジファンクションやCloudflare Workersなど、コールドスタート時のバンドルサイズが重要な環境で特に有利です。APIは関数型スタイルで、
pipeを使用します。 - Zod: 広く普及しており、tRPC、React Hook Form、OpenAPIジェネレーターなど、豊富なエコシステムが最大の強みです。APIはメソッドチェーンスタイルで、直感的で学習しやすいと評価されています。Zod v3.xでバンドルサイズとパフォーマンスが大幅に改善されていますが、Valibotには及ばない場合があります。
- トレードオフ: バンドルサイズを最優先するならValibot、既存のエコシステムとの連携やメソッドチェーンによる開発体験を重視するならZodが有力な選択肢となります。
スキーマ定義の一元化
- ベストプラクティス: 型定義とバリデーションロジックをZodやValibotのスキーマとして一元化することは、コードの重複を排除し、保守性を向上させる上で非常に有効です。これにより、「コードに書いた型 ≠ 実行時の実体」というTypeScriptにおける根本的な課題を解決し、コンパイル時とランタイムの整合性を高めることができます。
エラーハンドリング戦略
-
ベストプラクティス: API連携やユーザー入力のバリデーションでは、
parse()のような例外をスローするメソッドと、safeParse()のような結果オブジェクトを返すメソッドを適切に使い分けます。-
parse(): バリデーション失敗時に処理を即座に中断したい場合(例: サーバーサイドのAPIエンドポイントで不正なリクエストを早期に拒否する場合)に有効です。try-catchブロックでの例外処理が必要です。 -
safeParse(): エラー発生時にも処理を継続し、エラー情報をきめ細かく扱いたい場合(例: フォーム入力バリデーションで複数のエラーメッセージをユーザーに表示する場合)に有効です。
-
カスタムバリデーションと変換
-
ベストプラクティス: 組み込みのバリデーションルールだけでは対応できない複雑なビジネスロジックやデータ変換が必要な場合があります。
- Zodでは
refine()やsuperRefine()を使用して、カスタムのバリデーションロジックを追加できます。 - Valibotでは
check()やtransform()を使用して、同様の処理を行うことができます。
- Zodでは
まとめ
本記事では、TypeScriptプロジェクトにおいてランタイムバリデーションが不可欠である理由から、人気のライブラリであるZodとValibotの具体的な実装方法、よくある課題と解決策、そして選定のトレードオフについて解説しました。
- Zodは、豊富なエコシステムと直感的なメソッドチェーンAPIにより、開発体験と既存ツールとの連携を重視するプロジェクトに適しています。
- Valibotは、極めて小さなバンドルサイズと関数型APIを特徴とし、パフォーマンスやエッジコンピューティング環境での利用を重視するプロジェクトに強力な選択肢となります。
どちらのライブラリも、スキーマ定義からTypeScriptの型を自動生成することで、コンパイル時とランタイムの型安全性を両立させ、堅牢なアプリケーション開発に貢献します。あなたのプロジェクトの要件に合わせて最適なランタイムバリデーション戦略を選択し、より安全で保守性の高いTypeScriptアプリケーションを構築してください。
さらに深く学びたい場合は、各ライブラリの公式ドキュメントを参照することをお勧めします。