TypeScript × zodでバリデーション

🎄[and factory.inc Advent Calendar 2024](https://qiita.com/advent-calendar/2024/andfactory)15日目の記事です🎄

# はじめに
サイトやアプリで、ユーザーが情報を入力する場面って結構多いですよね。
会員登録、ログイン、購入機能、お問い合わせフォーム...
そこで入力内容の形式を制限するバリデーションの実装がお手軽にできて、TypeScriptと相性の良さそうなライブラリがあるので取り上げようと思います。

# zodとは

[zod公式ドキュメント](https://zod.dev/) によると「TypeScriptファーストのスキーマ宣言および検証ライブラリ」とのこと。TypeScriptファーストと記載してますがJavaScriptでも動くそうです。


# 使い方

```typescript
import { z } from "zod";

// スキーマ定義
const UserSchema = z.object({
  // 1文字以上の文字列
  name: z.string().min(1, '名前は必須です'),
  // 0以上の数値
  age: z.number().min(0, '年齢は0以上である必要があります'),
  // メールアドレスの形式かどうか
  email: z.string().email('有効なメールアドレスを入力してください'),
});

// 型定義を生成
type User = z.infer<typeof UserSchema>;

// 入力データ
const inputData: User = {
  name: 'John',
  age: 25,
  email: 'sample@example.com',
};

// パース処理（バリデーション）
const result = UserSchema.safeParse(inputData);

// safeParseの返り値を確認するため成功/失敗時の両方ともresultを返すようにしています
if (result.success) {
  console.log('バリデーション成功:', result);
} else {
  console.log('バリデーション失敗:', result);
}

```

### スキーマ定義
UserSchemaで扱う値（今回だとname, age, email）の形式を設定しています。
min()はstringなら最小文字数、number型なら最小値の指定になります。最大値の指定はmax()です。
型と文字数制限などの形式が同じ場所に書けるので、どんな値を扱うかパッと見わかりやすいです。


### 型の自動生成
z.infer<>では指定したスキーマから型を自動で生成してくれます。

```typescript 
type User = z.infer<typeof UserSchema>;
// ↓ 生成される型
// type User = {
//  name: string;
//  age: number;
//  email: string;
// };
```

実際に値を受け取りUser型を当てているinputDataと、zodで定義したUserSchemaの構造を簡単に一致させることができます。
スキーマを変更したら自動で型が更新されるのもありがたいです。

下記のようにnumber型を期待しているageにstring型の値をセットすると、バリデーション対象の値が期待した型と異なるとエディタ上でエラーが出ます。


```typescript
const inputData: User = {
  name: 'John',
  // エラー「型 'string' を型 'number' に割り当てることはできません。」
  age: '20',
  email: 'sample@example.com',
};
```

### メールアドレスの形式

許容される例
：``` user@example.com ```、```user@sub.domain.com、 ```
許容されない例
：``` user@-example.com ```、 ```user@domain..com ```、 ```user@domain.c ```、``` あいうえお@domain.com ```

特定の特殊文字（< > ( ) [ ] \ , ; : @ " 空白）を含まない、ドメイン名の末尾は英字2文字以上が必要、連続したドットは許容しない等の指定があり、``` xx@xx.xx ``` のような形式を満たしているかをチェックしています。

### パース処理

**safeParse使用時**
バリデーション成功時には```result.success```にtrueと成功データ```data```を、失敗時には```result.success```にfalseとエラー情報を持つオブジェクト```error```が返されます。

バリデーション成功時

```console
バリデーション成功: {
  "success": true,
  "data": {
    "name": "John",
    "age": 20,
    "email": "sample@domain.com"
  }
}
```

バリデーション失敗時

```console
バリデーション失敗: {
  "success": false,
  "error": {
    "issues": [
      {
        "code": "too_small",
        "minimum": 1,
        "type": "string",
        "inclusive": true,
        "exact": false,
        "message": "名前は必須です",
        "path": [
          "name"
        ]
      },
      {
        "code": "invalid_type",
        "expected": "number",
        "received": "string",
        "path": [
          "age"
        ],
        "message": "Expected number, received string"
      },
      {
        "validation": "email",
        "code": "invalid_string",
        "message": "有効なメールアドレスを入力してください",
        "path": [
          "email"
        ]
      }
    ],
    "name": "ZodError"
  }
}
```

<br>

**parse使用時**
バリデーション成功時には入力内容が、失敗時にはコンソールにエラーが表示されます。
safeParseの場合は例外をスローしないため、返されるオブジェクト内にあるsuccessプロパティがfalseなら失敗時という判定にしていましたが、
parseの場合は失敗時に例外をスローするので```try...catch```で結果を受け取れます。

```typescript:safeParseをparseに置き換えたコード
// 省略
try {
  console.log('バリデーション成功:', result);
} catch(error) {
  console.log(error);
}
```

バリデーション成功時

```console
バリデーション成功: {
  "name": "John",
  "age": 25,
  "email": "user.aaa@domain.com"
}
```

バリデーション失敗時

```console
index.mjs:587 Uncaught ZodError: [
  {
    "code": "too_small",
    "minimum": 1,
    "type": "string",
    "inclusive": true,
    "exact": false,
    "message": "名前は必須です",
    "path": [
      "name"
    ]
  },
  {
    "validation": "email",
    "code": "invalid_string",
    "message": "有効なメールアドレスを入力してください",
    "path": [
      "email"
    ]
  }
]
```

[エラー内容の詳細](https://zod.dev/ERROR_HANDLING)

# まとめ

zodは定義したスキーマをTypeScriptの型として利用（＝スキーマから型定義を自動生成）できるため、型の安全性を保ったバリデーションができます。
今回の例ではバリデーション対象は名前・年齢・メールアドレスの３つだけでしたが、その他にも日付や時刻、urlを検証するメソッドが用意されていますので、入力フォームの項目数が多くなっても自力でのバリデーション実装に比べると負担が軽くなるかもしれません。