1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-01-11

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

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

目次

  1. はじめに
  2. 必要なパッケージ
  3. 基本的な使用例
  4. 主要な機能の解説
  5. トラブルシューティング
  6. 発展的な使用例
  7. まとめ

はじめに

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

コードの解説

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

  1. スキーマの定義

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

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

    • 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"]
});

トラブルシューティング

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

  1. 型の不一致エラー

    // ❌ 問題のあるコード
    const schema = z.number();
    schema.parse("123"); // エラー:文字列を数値として解析できない
    // parseメソッドは、スキーマで定義された型と異なる型の値が入力された場合、エラーを発生させる
    
    // ✅ 解決策
    const schema = z.coerce.number(); // 文字列を数値に変換
    // coerceは入力値を指定した型に自動変換する特別なバリデーターを作成するZodのメソッド
    
    schema.parse("123"); // OK
    
  2. オプショナルフィールドの扱い

    // すべてのフィールドが必須項目
    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-formzodを組み合わせた実践的なフォーム実装について解説していきますので、ご期待ください!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?