T-CREATOR

Zod 全体像を図解で理解:プリミティブ → 合成 → 効果(transform/coerce/refine)の流れ

Zod 全体像を図解で理解:プリミティブ → 合成 → 効果(transform/coerce/refine)の流れ

TypeScript でのデータ検証において、Zod は開発者にとって非常に強力なライブラリです。しかし、その豊富な機能の全体像を把握するのは初心者にとって難しいことがあります。

本記事では、Zod の機能を「プリミティブ型」→「合成型」→「効果」という 3 つの段階に分けて、図解とともにわかりやすく解説いたします。この流れを理解することで、Zod の真の力を活用できるようになるでしょう。

Zod とは:TypeScript 型検証の革新

Zod は、TypeScript のためのスキーマ検証ライブラリです。従来のバリデーションライブラリとは異なり、TypeScript の型システムと密接に連携し、型安全性とランタイム検証を両立させます。

Zod の特徴

Zod が他のライブラリと異なる点は、以下の 3 つの特徴にあります。

typescriptimport { z } from 'zod';

// TypeScriptの型を自動推論
const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
});

// 型も同時に生成される
type User = z.infer<typeof UserSchema>;

図で理解する Zod の基本構造

mermaidflowchart TD
    primitive[プリミティブ型] --> compose[合成型]
    compose --> effect[効果]
    primitive --> |基本的な型定義| string[string, number, boolean]
    compose --> |複雑な構造| object[object, array, union]
    effect --> |高度な処理| transform[transform, coerce, refine]

この図が示すように、Zod は段階的に機能が積み重なる設計となっています。基礎から応用まで、一つずつ理解していくことが重要です。

段階機能役割
1プリミティブ型基本的なデータ型の検証
2合成型複雑なデータ構造の構築
3効果データ変換と高度な検証

プリミティブ型:Zod の基礎となる型定義

プリミティブ型は、Zod の最も基本的な構成要素です。文字列、数値、真偽値といった基本的なデータ型から始まります。

文字列・数値・真偽値の基本型

最初に、最もシンプルなプリミティブ型から見ていきましょう。

typescriptimport { z } from 'zod';

// 基本的なプリミティブ型の定義
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();

これらの基本型には、さらに詳細な制約を追加できます。

typescript// 文字列の制約
const emailSchema = z.string().email();
const minLengthSchema = z.string().min(3);
const maxLengthSchema = z.string().max(100);

// 数値の制約
const positiveSchema = z.number().positive();
const integerSchema = z.number().int();
const rangeSchema = z.number().min(0).max(100);

実際の検証処理では、以下のように使用します。

typescript// 検証の実行
const result = emailSchema.safeParse('user@example.com');

if (result.success) {
  console.log('有効なメール:', result.data);
} else {
  console.log('エラー:', result.error.issues);
}

配列・オブジェクトの基本構造

プリミティブ型を組み合わせて、配列やオブジェクトの構造を定義できます。

typescript// 配列の定義
const stringArraySchema = z.array(z.string());
const numberArraySchema = z.array(z.number());

// 基本的なオブジェクトの定義
const personSchema = z.object({
  name: z.string(),
  age: z.number(),
  isActive: z.boolean(),
});

より複雑な構造も段階的に構築できます。

typescript// ネストしたオブジェクト
const addressSchema = z.object({
  street: z.string(),
  city: z.string(),
  zipCode: z.string(),
});

const userWithAddressSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  address: addressSchema,
});

プリミティブ型の関係図

mermaidflowchart LR
    basic["基本型"] --> advanced["拡張型"];
    basic --> stringType["string"];
    basic --> numberType["number"];
    basic --> booleanType["boolean"];
    advanced --> arrayType["array"];
    advanced --> objectType["object"];
    stringType --|制約|--> email[".email()"];
    numberType --|制約|--> positive[".positive()"];
    arrayType --|要素型|--> stringArray["string[]"];
    objectType --|プロパティ|--> nested["ネストした構造"];

プリミティブ型の段階では、個々のデータ型とその基本的な制約を理解することが重要です。この土台があることで、次の合成型での複雑な構造構築が可能になります。

合成型:複雑なデータ構造の構築

合成型では、プリミティブ型を組み合わせて、より複雑で実用的なデータ構造を作成します。これにより、実際のアプリケーションで使用される複雑なデータモデルを表現できるようになります。

オブジェクトスキーマの組み合わせ

複数のオブジェクトスキーマを組み合わせることで、保守性の高いスキーマ設計が可能です。

typescript// 基本的なスキーマ要素
const contactInfoSchema = z.object({
  email: z.string().email(),
  phone: z.string().optional(),
});

const personalInfoSchema = z.object({
  firstName: z.string(),
  lastName: z.string(),
  birthDate: z.string().datetime(),
});

これらの基本要素を組み合わせて、より大きなスキーマを構築します。

typescript// スキーマの合成
const userProfileSchema = z.object({
  id: z.string().uuid(),
  personal: personalInfoSchema,
  contact: contactInfoSchema,
  preferences: z.object({
    language: z.enum(['ja', 'en', 'zh']),
    timezone: z.string(),
    notifications: z.boolean(),
  }),
});

実際の使用例では、部分的な更新も可能です。

typescript// 部分的な更新用スキーマ
const updateUserSchema = userProfileSchema.partial();

// 特定フィールドのみ必須
const essentialUserSchema = userProfileSchema.pick({
  id: true,
  personal: true,
});

Union・Intersection 型の活用

Union 型と Intersection 型を使用することで、柔軟なデータ構造を表現できます。

typescript// Union型:複数の型のいずれか
const paymentMethodSchema = z.union([
  z.object({
    type: z.literal('credit_card'),
    cardNumber: z.string(),
    expiryDate: z.string(),
  }),
  z.object({
    type: z.literal('bank_transfer'),
    accountNumber: z.string(),
    routingNumber: z.string(),
  }),
]);

Intersection 型では、複数の型を組み合わせます。

typescript// Intersection型:複数の型を結合
const timestampSchema = z.object({
  createdAt: z.string().datetime(),
  updatedAt: z.string().datetime(),
});

const auditableUserSchema = z.intersection(
  userProfileSchema,
  timestampSchema
);

より実践的な例として、API レスポンスの型定義を見てみましょう。

typescript// APIレスポンスの合成型
const apiResponseSchema = z.union([
  z.object({
    success: z.literal(true),
    data: userProfileSchema,
  }),
  z.object({
    success: z.literal(false),
    error: z.object({
      code: z.string(),
      message: z.string(),
    }),
  }),
]);

合成型の構造図

mermaidflowchart TD
    compose[合成型] --> union[Union型]
    compose --> intersection[Intersection型]
    compose --> combination[組み合わせ]

    union --> |選択| payment[決済方法]
    union --> |条件分岐| response[APIレスポンス]

    intersection --> |結合| audit[監査可能なユーザー]
    intersection --> |拡張| enhanced[拡張された型]

    combination --> |組み立て| profile[ユーザープロフィール]
    combination --> |再利用| modular[モジュラー設計]

合成型により、コードの再利用性と保守性が大幅に向上します。基本的な要素を組み合わせることで、複雑なビジネスロジックを型安全に表現できるのです。

効果:データ変換と検証の拡張

効果は、Zod の最も強力な機能の一つです。単純な検証を超えて、データの変換、型の自動変換、カスタム検証ロジックを実装できます。

transform:データの変換処理

transform メソッドを使用することで、検証と同時にデータの変換を行えます。

typescript// 文字列を数値に変換
const stringToNumberSchema = z.string().transform((str) => {
  return parseInt(str, 10);
});

// 日付文字列をDateオブジェクトに変換
const dateTransformSchema = z
  .string()
  .transform((dateStr) => {
    return new Date(dateStr);
  });

より複雑な変換処理も可能です。

typescript// オブジェクトの構造変換
const apiUserToInternalUserSchema = z
  .object({
    user_name: z.string(),
    user_email: z.string().email(),
    user_age: z.number(),
  })
  .transform((apiUser) => ({
    name: apiUser.user_name,
    email: apiUser.user_email,
    age: apiUser.user_age,
    id: crypto.randomUUID(), // 新しいフィールドを追加
  }));

配列に対する変換も効果的です。

typescript// CSV文字列を配列に変換
const csvToArraySchema = z.string().transform((csv) => {
  return csv.split(',').map((item) => item.trim());
});

// 検証と変換を組み合わせ
const validatedArraySchema = csvToArraySchema.transform(
  (arr) => {
    return arr.filter((item) => item.length > 0);
  }
);

coerce:型の自動変換

coerce は、JavaScript 特有の型変換を安全に行うための機能です。

typescript// 文字列を数値に強制変換
const coercedNumberSchema = z.coerce.number();

// 実行例
console.log(coercedNumberSchema.parse('123')); // 123 (数値)
console.log(coercedNumberSchema.parse(true)); // 1 (数値)

日付の変換では特に有用です。

typescript// 様々な形式を Date に変換
const coercedDateSchema = z.coerce.date();

// これらすべてがDateオブジェクトになる
coercedDateSchema.parse('2024-01-01'); // Date オブジェクト
coercedDateSchema.parse(1704067200000); // Date オブジェクト
coercedDateSchema.parse(new Date()); // Date オブジェクト

真偽値の変換も直感的です。

typescript// 文字列を真偽値に変換
const coercedBooleanSchema = z.coerce.boolean();

console.log(coercedBooleanSchema.parse('true')); // true
console.log(coercedBooleanSchema.parse('false')); // false
console.log(coercedBooleanSchema.parse('')); // false
console.log(coercedBooleanSchema.parse('1')); // true

refine:カスタム検証ロジック

refine メソッドでは、独自の検証ルールを追加できます。

typescript// パスワードの複雑性チェック
const passwordSchema = z
  .string()
  .min(8, 'パスワードは8文字以上である必要があります')
  .refine(
    (password) => /[A-Z]/.test(password),
    '大文字を含む必要があります'
  )
  .refine(
    (password) => /[0-9]/.test(password),
    '数字を含む必要があります'
  );

オブジェクト全体に対する検証も可能です。

typescript// 期間の妥当性チェック
const eventSchema = z
  .object({
    startDate: z.string().datetime(),
    endDate: z.string().datetime(),
    title: z.string(),
  })
  .refine(
    (event) =>
      new Date(event.startDate) < new Date(event.endDate),
    {
      message: '開始日は終了日より前である必要があります',
      path: ['endDate'], // エラーの対象フィールドを指定
    }
  );

非同期検証にも対応しています。

typescript// データベースでの重複チェック
const uniqueEmailSchema = z
  .string()
  .email()
  .refine(async (email) => {
    // データベースでの検索をシミュレート
    const existingUser = await checkEmailExists(email);
    return !existingUser;
  }, 'このメールアドレスは既に使用されています');

効果システムの処理フロー

mermaidsequenceDiagram
    participant Input as 入力データ
    participant Validate as 基本検証
    participant Transform as transform
    participant Coerce as coerce
    participant Refine as refine
    participant Output as 出力データ

    Input ->> Validate: データ受信
    Validate ->> Coerce: 型変換
    Coerce ->> Transform: データ変換
    Transform ->> Refine: カスタム検証
    Refine ->> Output: 最終データ

    Note over Input, Output: エラーが発生した場合はここで処理が停止

効果を組み合わせることで、非常に柔軟で強力なデータ処理パイプラインを構築できます。入力から出力まで、一貫した型安全性を保ちながら、複雑な変換と検証を実現できるのです。

実践例:プリミティブから効果まで一貫した使い方

これまでに学んだプリミティブ型、合成型、効果を組み合わせて、実際の Web アプリケーションで使用される複雑なフォームバリデーションシステムを構築してみましょう。

ユーザー登録フォームの完全実装

まず、プリミティブ型から始めて段階的に構築していきます。

typescriptimport { z } from 'zod';

// 1. プリミティブ型の定義
const emailSchema = z
  .string()
  .email('有効なメールアドレスを入力してください');
const passwordSchema = z
  .string()
  .min(8, 'パスワードは8文字以上必要です');
const ageSchema = z
  .number()
  .min(13, '13歳以上である必要があります');

次に、合成型を使用してより複雑な構造を作成します。

typescript// 2. 合成型の構築
const addressSchema = z.object({
  country: z.string(),
  prefecture: z.string(),
  city: z.string(),
  street: z.string(),
  zipCode: z
    .string()
    .regex(
      /^\d{3}-\d{4}$/,
      '郵便番号は123-4567の形式で入力してください'
    ),
});

const personalInfoSchema = z.object({
  firstName: z.string().min(1, '名前を入力してください'),
  lastName: z.string().min(1, '苗字を入力してください'),
  birthDate: z.string().datetime(),
  gender: z.enum([
    'male',
    'female',
    'other',
    'prefer_not_to_say',
  ]),
});

そして、効果を適用して高度な処理を実装します。

typescript// 3. 効果の適用
const userRegistrationSchema = z
  .object({
    // 基本情報
    email: emailSchema,
    password: passwordSchema,
    passwordConfirm: z.string(),

    // 個人情報(変換付き)
    personal: personalInfoSchema.transform((data) => ({
      ...data,
      fullName: `${data.lastName} ${data.firstName}`,
      age: calculateAge(new Date(data.birthDate)),
    })),

    // 住所(オプショナル)
    address: addressSchema.optional(),

    // 利用規約同意(型強制)
    agreeToTerms: z.coerce.boolean(),

    // 年齢(計算フィールド)
    calculatedAge: z.number().optional(),
  })
  // パスワード確認の検証
  .refine(
    (data) => data.password === data.passwordConfirm,
    {
      message: 'パスワードが一致しません',
      path: ['passwordConfirm'],
    }
  )
  // メール重複チェック
  .refine(
    async (data) => {
      const exists = await checkEmailExists(data.email);
      return !exists;
    },
    {
      message: 'このメールアドレスは既に登録されています',
      path: ['email'],
    }
  )
  // 最終的なデータ変換
  .transform((data) => ({
    ...data,
    id: crypto.randomUUID(),
    createdAt: new Date().toISOString(),
    isEmailVerified: false,
  }));

エラーハンドリングと型推論

完全な型安全性を保ちながらエラーハンドリングを実装します。

typescript// 型の推論
type UserRegistrationInput = z.input<
  typeof userRegistrationSchema
>;
type UserRegistrationOutput = z.output<
  typeof userRegistrationSchema
>;

// フォーム送信処理
async function handleUserRegistration(formData: unknown) {
  try {
    // 検証と変換の実行
    const validatedData =
      await userRegistrationSchema.parseAsync(formData);

    // データベースへの保存
    const savedUser = await saveUserToDatabase(
      validatedData
    );

    return {
      success: true,
      data: savedUser,
    };
  } catch (error) {
    if (error instanceof z.ZodError) {
      // Zodのエラーを整形
      const formattedErrors = error.issues.map((issue) => ({
        field: issue.path.join('.'),
        message: issue.message,
        code: issue.code,
      }));

      return {
        success: false,
        errors: formattedErrors,
      };
    }

    // その他のエラー
    throw error;
  }
}

API 統合での活用

API エンドポイントでの完全な活用例を見てみましょう。

typescript// APIリクエスト/レスポンスの定義
const apiRequestSchema = z.object({
  action: z.enum(['create', 'update', 'delete']),
  data: userRegistrationSchema,
  metadata: z
    .object({
      clientVersion: z.string(),
      timestamp: z.coerce.date(),
    })
    .optional(),
});

const apiResponseSchema = z.union([
  z.object({
    success: z.literal(true),
    data: z.object({
      id: z.string().uuid(),
      email: z.string().email(),
      createdAt: z.string().datetime(),
    }),
  }),
  z.object({
    success: z.literal(false),
    error: z.object({
      code: z.string(),
      message: z.string(),
      details: z
        .array(
          z.object({
            field: z.string(),
            message: z.string(),
          })
        )
        .optional(),
    }),
  }),
]);

実践例の全体フロー

mermaidflowchart TD
    input[フォーム入力] --> primitive[プリミティブ検証]
    primitive --> compose[合成型構築]
    compose --> effect[効果適用]

    effect --> transform[データ変換]
    effect --> coerce[型強制]
    effect --> refine[カスタム検証]

    transform --> output[最終データ]
    coerce --> output
    refine --> output

    output --> api[API送信]
    api --> response[レスポンス処理]

    primitive --> |エラー| error[エラーハンドリング]
    compose --> |エラー| error
    effect --> |エラー| error
    error --> user[ユーザーへの表示]

この実践例では、単純なプリミティブ型から始まり、合成型で構造を構築し、効果で高度な処理を実装する流れを示しています。実際のアプリケーションでも、この段階的なアプローチにより、保守性と型安全性を両立させた堅牢なシステムを構築できます。

まとめ

本記事では、Zod の全体像を「プリミティブ型」→「合成型」→「効果」という 3 つの段階に分けて解説してまいりました。

学習した内容の振り返り

プリミティブ型では、Zod の基礎となる型定義を学びました。文字列、数値、真偽値といった基本型から、配列やオブジェクトの基本構造まで、型安全な検証の土台を理解できました。

合成型では、複雑なデータ構造の構築方法を習得しました。オブジェクトスキーマの組み合わせや、Union・Intersection 型の活用により、実際のアプリケーションで使用される複雑なデータモデルを表現する技術を身につけました。

効果では、データ変換と検証の拡張について深く学びました。transform、coerce、refine の 3 つの機能により、単純な検証を超えた高度なデータ処理が可能になることを理解しました。

Zod を使用する際のベストプラクティス

これらの学習を踏まえて、以下のポイントを意識して開発を進めることをお勧めいたします。

  1. 段階的な構築: プリミティブ型から始めて、必要に応じて合成型、効果を追加する
  2. 再利用可能な設計: 基本的なスキーマ要素を定義し、組み合わせて使用する
  3. 適切なエラーハンドリング: Zod のエラー情報を活用して、ユーザーフレンドリーなメッセージを提供する
  4. 型推論の活用: z.inferを使用して、TypeScript の型を自動生成する

今後の発展

Zod をマスターすることで、TypeScript での開発における型安全性が大幅に向上します。フォームバリデーション、API 通信、データベースとの連携など、あらゆる場面でその力を発揮することでしょう。

継続的な学習により、より効率的で保守性の高いアプリケーション開発が実現できます。ぜひ実際のプロジェクトで Zod を活用し、その利便性を体感してください。

関連リンク