TypeScript × zod 入門 【第2回:バリデーション編】
私はNext.js
とtailwindcss
を使用し、フロントエンド開発を行っている初心者です。フォームのバリデーション(入力値の検証)実装において、型安全性の確保と実行時のエラーチェックの両立に苦労していました。そこで、zod
の基本から応用まで、3回シリーズで解説していきたいと思います。
- 第1回:react-hook-formの基本実装
- 第2回:zodによるバリデーション(本記事)
- 第3回:react-hook-formとzodの連携
目次
はじめに
Webアプリケーションにおけるデータのバリデーション(入力値の検証)は、ユーザー体験と信頼性を確保する上で重要な要素です。
特にTypeScriptを使用する環境では、コンパイル時の型チェックと実行時のバリデーションの両立が課題となります。zod
は、TypeScriptのための型定義とバリデーションを統合的に扱えるライブラリで、直感的なAPIと高い型安全性を提供します。
今回は、zod
の基本的な使い方から実践的なバリデーションパターンまでを、具体的な例を交えて解説していきます。
必要なパッケージ
npm
npm install zod
yarn
yarn add zod
基本的な使用例
スキーマの書き方
zod
では、データの構造とバリデーションルールを「スキーマ」として定義します。スキーマとはデータの形や制約を定義したものです。
import { z } from "zod";
// フォームデータの型定義
const userSchema = z.object({
username: z.string()
.min(3, "ユーザー名は3文字以上で入力してください")
.max(20, "ユーザー名は20文字以内で入力してください"),
email: z.string()
.email("無効なメールアドレス形式です"),
age: z.number()
.min(18, "18歳以上である必要があります")
.max(120, "有効な年齢を入力してください"),
password: z.string()
.min(8, "パスワードは8文字以上で入力してください")
.regex(/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/,
"パスワードは英字と数字を含む必要があります"),
});
// スキーマから型を生成
type UserForm = z.infer<typeof userSchema>;
コードの解説
このコード例の基本的な流れを解説します。
-
スキーマの定義
- スキーマは
z.object()
を使用して、オブジェクトの構造を定義します - オブジェクトの各プロパティに対して、型とバリデーションルールを設定します
- スキーマは
-
バリデーションルールの設定
- 各フィールドに対して、必要なバリデーションルールを設定します
- エラーメッセージは、各ルールの第二引数として指定できます
-
型の生成
-
z.infer<typeof schema>
を使用して、スキーマから自動的にTypeScriptの型を生成します -
infer
は、「既存のコードや定義から適切な型を推論して生成する」ための指示です - 生成された型は、フォームデータの型チェックに使用できます
- このスキーマから生成される型は以下のようになります
type UserForm = { username: string; email: string; age: number; password: string; }
-
主要な機能の解説
基本的なバリデーター
// 文字列のバリデーション
const stringSchema = z.string()
.min(1, "入力必須です")
.max(100, "100文字以内で入力してください")
.regex(/^[A-Za-z0-9]+$/, "英数字のみ使用可能です");
// 数値のバリデーション
const numberSchema = z.number()
.positive("正の数を入力してください")
.int("整数を入力してください")
.safe("Number.MAX_SAFE_INTEGER以内の値を入力してください");
// JavaScriptに組み込まれている定数 Number.MAX_SAFE_INTEGER (2^53 - 1 = 9007199254740991) 以下の値のみ許可
// この値を超えると計算の精度が保証されなくなるため、安全な整数の上限として使用されます
// 真偽値のバリデーション
const booleanSchema = z.boolean()
.default(false); // 値が未定義の場合、初期値 false
// 日付のバリデーション
const dateSchema = z.date()
.min(new Date("2000-01-01"), "2000年以降の日付を入力してください");
バリデーションルールの種類
1. 基本的なルール
z.string() // 文字列
z.number() // 数値
z.boolean() // 真偽値
z.date() // 日付
z.array(...) // 配列
z.object({...}) // オブジェクト
z.enum([...]) // 列挙型
2. オプション設定
.optional() // 任意項目
.nullable() // null許可
.default(value) // デフォルト値
3. 文字列のルール
.min(length) // 最小文字数
.max(length) // 最大文字数
.email() // メールアドレス形式
.url() // URL形式
.regex(pattern) // 正規表現
4. 数値のルール
.min(value) // 最小値
.max(value) // 最大値
.positive() // 正の数
.negative() // 負の数
.int() // 整数
条件付きバリデーション
// 従業員情報のバリデーションスキーマを定義
const conditionalSchema = z.object({
// 雇用形態を「正社員」「契約社員」「アルバイト」のいずれかに制限
employmentType: z.enum(["正社員", "契約社員", "アルバイト"]),
// 勤続年数を数値型で定義(任意項目)
yearsOfService: z.number().optional(),
// 契約終了日を日付型で定義(任意項目)
contractEndDate: z.date().optional(),
// 追加のバリデーションルールを定義
// スキーマのrefineメソッドで、契約社員の場合は契約終了日が必須であるという条件を定義
// refineメソッド:スキーマの検証ロジックをカスタマイズするメソッド
// 引数に関数を取り、その関数の戻り値がtrueであれば検証をパス、falseであれば検証エラー
}).refine((data) => {
// 雇用形態が契約社員の場合の条件チェック
if (data.employmentType === "契約社員") {
// 契約社員の場合は契約終了日が必須
return data.contractEndDate !== undefined;
}
// 契約社員以外は制約なし
return true;
}, {
// バリデーションエラー時のメッセージを設定
message: "契約社員の場合、契約終了日は必須です",
// エラー項目を契約終了日に指定
path: ["contractEndDate"]
});
トラブルシューティング
よくあるエラーと解決方法
-
型の不一致エラー
// ❌ 問題のあるコード const schema = z.number(); schema.parse("123"); // エラー:文字列を数値として解析できない // parseメソッドは、スキーマで定義された型と異なる型の値が入力された場合、エラーを発生させる // ✅ 解決策 const schema = z.coerce.number(); // 文字列を数値に変換 // coerceは入力値を指定した型に自動変換する特別なバリデーターを作成するZodのメソッド schema.parse("123"); // OK
-
オプショナルフィールドの扱い
// すべてのフィールドが必須項目 const schema = z.object({ name: z.string(), age: z.number() // ageは数値型として定義(必須項目) }); // age項目のみ任意のスキーマ定義 const schema = z.object({ name: z.string(), age: z.number().optional() // ageは数値型で、任意項目(なくても良い)として定義 }); // すべてのフィールドを任意項目にするスキーマ定義 const schema = z.object({...}).partial();
実装時の注意点
- スキーマ定義は型定義と一致させる
- エラーメッセージは具体的に設定する
- バリデーションの順序に注意する
発展的な使用例
複雑なフォームのバリデーション
// 住所フォームの例
const addressSchema = z.object({
// 郵便番号: 7桁の数字のみを受け付ける文字列型(数値型だと先頭の0が失われる可能性があるので文字列型が適切)
postalCode: z.string()
// 正規表現: 7桁の数字のみ許可 (例: "1234567" はOK, "123-4567", "123456" はNG)
// 文字列の最初(^)から最後(\$)まで、数字(\d)が7文字({7})
.regex(/^\d{7}$/, "7桁の数字で入力してください"),
// 都道府県: 1文字以上必須の文字列型
prefecture: z.string()
.min(1, "都道府県を選択してください"),
// 市区町村: 1文字以上必須の文字列型
city: z.string()
.min(1, "市区町村を入力してください"),
// 番地: 1文字以上必須の文字列型
street: z.string()
.min(1, "番地を入力してください"),
// 建物名: 文字列型(任意)
building: z.string().optional(),
});
// 注文フォームの例
const orderSchema = z.object({
// 商品リスト: オブジェクトの配列で、各オブジェクトは商品ID(文字列)と数量(正の数)
items: z.array(z.object({
// 商品ID: 文字列型
productId: z.string(),
// 数量: 正の数である必要がある数値型
quantity: z.number()
.positive("数量は1以上を入力してください")
}))
// 商品は1つ以上必要
.min(1, "1つ以上の商品を選択してください"),
// 配送先住所: addressSchemaで定義された型
shippingAddress: addressSchema,
// 請求先住所: addressSchemaで定義された型(任意)
billingAddress: addressSchema.optional(),
// 配送先住所を請求先住所として使用するかどうか: 真偽値
useShippingAsBilling: z.boolean(),
// 注文フォーム全体の検証: useShippingAsBillingがfalseの場合、請求先住所が入力されているか検証
}).refine((data) => {
// useShippingAsBillingがfalseの場合、請求先住所が存在するかを確認
if (!data.useShippingAsBilling) {
return data.billingAddress !== undefined;
}
// useShippingAsBillingがtrueの場合は検証は不要
return true;
}, {
// エラーメッセージ: 請求先住所が未入力の場合に表示
message: "請求先住所を入力してください",
// エラーのパス: どのフィールドでエラーが発生したかを示す
path: ["billingAddress"]
});
スキーマの合成と再利用
// 基本的な連絡先情報のスキーマ
const contactSchema = z.object({
// メールアドレス: 有効なメールアドレス形式である必要がある文字列型
email: z.string().email(),
// 電話番号: 10桁または11桁の数字のみを受け付ける文字列型
phone: z.string().regex(/^\d{10,11}$/)
});
// 個人情報のスキーマ
const personalInfoSchema = z.object({
// 名: 文字列型
firstName: z.string(),
// 姓: 文字列型
lastName: z.string(),
// 誕生日: Date型
birthDate: z.date()
});
// ユーザープロフィールのスキーマ(スキーマの合成)
const userProfileSchema = z.object({
// ID: UUID形式(ランダムな識別子)である必要がある文字列型
// 例:123c5678-q10i-23c5-b789-123456789000のような一意の識別子
id: z.string().uuid(),
// contactSchemaの各フィールドをuserProfileSchemaに展開
// ※バリデーションルールも含めてすべてのプロパティを継承
// .shape: 既存のZodスキーマからバリデーションルールを含むすべてのフィールド定義を抽出するメソッド
...contactSchema.shape,
// personalInfoSchemaのスキーマからフィールド定義を取り出し、オブジェクトとして展開
...personalInfoSchema.shape,
// 設定情報: オブジェクト型
preferences: z.object({
// ニュースレター購読設定: 真偽値
newsletter: z.boolean(),
// 通知設定: 真偽値
notifications: z.boolean()
})
});
まとめ
本記事では、zod
の基本的な使い方について解説しました。TypeScriptと統合された強力なバリデーション機能により、型安全性とバリデーションを一元的に管理できることが特徴です。特に、フォーム開発やAPI通信におけるデータ検証において、その効果を発揮します。
もし記事の内容に間違いや改善点がありましたら、コメントでご指摘いただけますと幸いです。また、実務での活用事例やベストプラクティスについても、ぜひ共有していただければと思います。
次回の記事では、react-hook-form
とzod
を組み合わせた実践的なフォーム実装について解説していきますので、ご期待ください!