🔰【初心者向け】TypeScript × zod フォーム入門（バリデーション編）🚀

# TypeScript × zod 入門 【第2回：バリデーション編】

私は`Next.js`と`tailwindcss`を使用し、フロントエンド開発を行っている初心者です。フォームのバリデーション（入力値の検証）実装において、型安全性の確保と実行時のエラーチェックの両立に苦労していました。そこで、`zod`の基本から応用まで、3回シリーズで解説していきたいと思います。

- [第1回：react-hook-formの基本実装](https://qiita.com/CodeLeaf/items/f4c0564293acb91139a1)
- 第2回：zodによるバリデーション（本記事）
- [第3回：react-hook-formとzodの連携](https://qiita.com/CodeLeaf/items/78b422ef5b388e6490c9)

## 目次
1. [はじめに](#はじめに)
1. [必要なパッケージ](#必要なパッケージ)
1. [基本的な使用例](#基本的な使用例)
1. [主要な機能の解説](#主要な機能の解説)
1. [トラブルシューティング](#トラブルシューティング)
1. [発展的な使用例](#発展的な使用例)
1. [まとめ](#まとめ)

## はじめに
Webアプリケーションにおけるデータのバリデーション（入力値の検証）は、ユーザー体験と信頼性を確保する上で重要な要素です。
特にTypeScriptを使用する環境では、コンパイル時の型チェックと実行時のバリデーションの両立が課題となります。`zod`は、TypeScriptのための型定義とバリデーションを統合的に扱えるライブラリで、直感的なAPIと高い型安全性を提供します。
今回は、`zod`の基本的な使い方から実践的なバリデーションパターンまでを、具体的な例を交えて解説していきます。

## 必要なパッケージ

### npm
```bash
npm install zod
```

### yarn
```bash
yarn add zod
```

## 基本的な使用例

### スキーマの書き方

`zod`では、データの構造とバリデーションルールを「スキーマ」として定義します。スキーマとはデータの形や制約を定義したものです。

```typescript
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>;
```

### コードの解説

このコード例の基本的な流れを解説します。

1. **スキーマの定義**
   - スキーマは`z.object()`を使用して、オブジェクトの構造を定義します
   - オブジェクトの各プロパティに対して、型とバリデーションルールを設定します

2. **バリデーションルールの設定**
   - 各フィールドに対して、必要なバリデーションルールを設定します
   - エラーメッセージは、各ルールの第二引数として指定できます

3. **型の生成**
   - `z.infer<typeof schema>`を使用して、スキーマから自動的にTypeScriptの型を生成します
   - `infer`は、「既存のコードや定義から適切な型を推論して生成する」ための指示です
   - 生成された型は、フォームデータの型チェックに使用できます
   - このスキーマから生成される型は以下のようになります
    ```typescript
    type UserForm = {
      username: string;
      email: string;
      age: number;
      password: string;
    }
    ```

## 主要な機能の解説

### 基本的なバリデーター

```typescript
// 文字列のバリデーション
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. 基本的なルール
```typescript
z.string()        // 文字列
z.number()        // 数値
z.boolean()       // 真偽値
z.date()          // 日付
z.array(...)      // 配列
z.object({...})   // オブジェクト
z.enum([...])     // 列挙型
```

#### 2. オプション設定
```typescript
.optional()       // 任意項目
.nullable()       // null許可
.default(value)   // デフォルト値
```

#### 3. 文字列のルール
```typescript
.min(length)      // 最小文字数
.max(length)      // 最大文字数
.email()          // メールアドレス形式
.url()            // URL形式
.regex(pattern)   // 正規表現
```

#### 4. 数値のルール
```typescript
.min(value)       // 最小値
.max(value)       // 最大値
.positive()       // 正の数
.negative()       // 負の数
.int()            // 整数
```

### 条件付きバリデーション

```typescript
// 従業員情報のバリデーションスキーマを定義
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"]
});
```

## トラブルシューティング

### よくあるエラーと解決方法

1. **型の不一致エラー**
    ```typescript
    // ❌ 問題のあるコード
    const schema = z.number();
    schema.parse("123"); // エラー：文字列を数値として解析できない
    // parseメソッドは、スキーマで定義された型と異なる型の値が入力された場合、エラーを発生させる

    // ✅ 解決策
    const schema = z.coerce.number(); // 文字列を数値に変換
    // coerceは入力値を指定した型に自動変換する特別なバリデーターを作成するZodのメソッド
    
    schema.parse("123"); // OK
    ```

2. **オプショナルフィールドの扱い**
    ```typescript
    // すべてのフィールドが必須項目
    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();
    ```

### 実装時の注意点

- スキーマ定義は型定義と一致させる
- エラーメッセージは具体的に設定する
- バリデーションの順序に注意する

## 発展的な使用例

### 複雑なフォームのバリデーション

```typescript
// 住所フォームの例
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"]
});
```

### スキーマの合成と再利用

```typescript
// 基本的な連絡先情報のスキーマ
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`を組み合わせた実践的なフォーム実装について解説していきますので、ご期待ください！