Zodで変わるTypeScript開発:モダンなスキーマバリデーションの全て
はじめに:TypeScript開発におけるデータ検証の課題とZodの登場
現代のソフトウェア開発において、データの信頼性はシステムの安定性やセキュリティの根幹をなします。特に、異なるシステム間でのデータのやり取り(API通信)、ユーザーからの入力、設定ファイルの読み込みなど、外部から供給されるデータは、常に期待通りの形式や値であるとは限りません。これらのデータを適切に検証せずに処理を進めると、予期せぬエラー、セキュリティ脆弱性、さらにはシステム全体のダウンにつながる可能性があります。
JavaScriptやTypeScriptの世界も例外ではありません。特に動的なJavaScriptの性質上、実行時までデータの型や構造が確定しないことが多く、これがバグの温床となりがちでした。TypeScriptは静的型付けによってこの問題をある程度緩和しますが、これはあくまで「開発時の」型チェックに過ぎません。ネットワーク越しに送られてくるJSONデータや、ファイルから読み込んだ設定オブジェクトなど、実行時に初めてその具体的な値が明らかになるデータに対しては、TypeScriptの型システムは直接的に「その値が本当にこの型定義を満たしているか」を保証できません。
従来のJavaScript/TypeScriptにおけるデータ検証アプローチには、いくつかの課題がありました。
- 手動での検証コード: データの各フィールドに対してif文などで個別にチェックを行う方法は、コードが冗長になりやすく、複雑な構造や多様な制約(最小値、最大値、正規表現マッチなど)を持つデータの検証はすぐに手に負えなくなります。また、検証ロジックとビジネスロジックが混在し、コードの見通しが悪化します。
- 型定義との乖離: 手動で検証コードを書いた場合、TypeScriptの型定義は別途記述する必要があります。しかし、検証ロジックと型定義は常に同期している必要がありますが、コードの変更に伴って片方だけが更新され、両者の間に乖離が生じやすいという問題がありました。これにより、「検証は通るが型エラーになる」「型は正しいが検証をすり抜ける」といった不整合が発生し、開発者が混乱しやすくなります。
- 既存のバリデーションライブラリの限界: これまでも多くのバリデーションライブラリ(Yup, Joi, Validator.jsなど)が存在しました。これらは検証ロジックの記述を効率化する一方で、TypeScriptとの連携が十分に考慮されていない場合や、TypeScriptの型推論をうまく活用できないという課題がありました。特に、バリデーションスキーマを定義した後に、そのスキーマに対応するTypeScriptの型定義を別途手で書く必要がある、あるいは複雑なユーティリティを使って型を生成する必要があるなど、TypeScriptネイティブな開発フローに完全にフィットしないケースが見られました。
このような背景から、TypeScript開発者がより快適かつ安全にデータ検証を行うための、モダンなライブラリが求められていました。そこで登場したのが Zod です。
Zodは「TypeScriptファースト」を謳うバリデーションライブラリです。その最大の特徴は、Zodスキーマを定義することで、実行時バリデーションロジックと対応するTypeScriptの型定義の両方を同時に、かつ型安全に記述できる 点にあります。Zodで定義されたスキーマからは、TypeScriptの強力な型推論機能によって自動的に対応する型が生成されます。これにより、データ検証と型定義の間に乖離が生じる心配がなくなり、開発者は安心してコードを書くことができます。
Zodはスキーマ定義のための直感的で表現力豊かなAPIを提供しており、基本的な型から複雑なオブジェクト、配列、さらにはユニオン型やディスクリミネートユニオンといった高度な型構造まで、柔軟に記述できます。また、詳細なエラーメッセージ、カスタムバリデーション、データ変換機能など、実践的な開発で必要となる多くの機能が備わっています。
本記事では、このZodの全てを詳細に解説します。Zodの基本的な使い方から始め、TypeScriptとの連携、高度な機能、そして実際のアプリケーション開発における実践的な利用例まで、ZodがいかにTypeScript開発を変革するのかを明らかにしていきます。約5000語にわたる詳細な説明を通じて、読者の皆様がZodを最大限に活用し、より堅牢で保守性の高いTypeScriptアプリケーションを構築できるようになることを目指します。
さあ、モダンなスキーマバリデーションの全てをZodと共に探求しましょう。
Zodの基本:インストールから最初のスキーマ定義まで
Zodを使うための最初のステップは、プロジェクトへのインストールです。Zodはnpmやyarnなどのパッケージマネージャーを使って簡単にインストールできます。
“`bash
npm install zod
または
yarn add zod
または
pnpm add zod
“`
Zodは純粋なTypeScriptで書かれており、外部依存がほとんどありません。インストールが完了したら、Zodをインポートしてスキーマ定義を始めることができます。
スキーマとは何か?
Zodにおける「スキーマ」とは、データの構造、型、および満たすべき制約を定義したオブジェクトのことです。例えば、「これは文字列であり、空であってはならない」とか、「これは数値の配列であり、各要素は正の整数でなければならない」、「これはname(文字列)とage(数値)を持つオブジェクトである」といったルールをスキーマとして表現します。
Zodは z という名前空間(多くの場合は import { z } from 'zod' のようにインポートします)を通じて、様々な種類のスキーマを定義するためのメソッドを提供します。
基本的なスキーマタイプ
ZodはJavaScript/TypeScriptの基本的な型に対応するスキーマタイプを提供しています。
- 文字列 (string):
z.string() - 数値 (number):
z.number() - 真偽値 (boolean):
z.boolean() - 日付 (date):
z.date() - null:
z.null() - undefined:
z.undefined() - void:
z.void()(関数の戻り値がvoidであることを示すためなど) - any:
z.any()(非推奨、型の安全性を損なう可能性があるため注意) - unknown:
z.unknown()(anyより安全、バリデーション後に型を絞り込む必要がある) - never:
z.never()(決して起こり得ない型、到達不能なコードパスを示すためなど)
これらの基本的なスキーマには、さらに制約を追加するためのメソッドが用意されています。
例えば、z.string() には以下のようなメソッドがあります。
.min(minLength, { message }): 最小文字数を指定.max(maxLength, { message }): 最大文字数を指定.length(length, { message }): 固定文字数を指定.email({ message }): 有効なメールアドレス形式であるかチェック.url({ message }): 有効なURL形式であるかチェック.uuid({ message }): 有効なUUID形式であるかチェック.cuid({ message }): 有効なCUID形式であるかチェック.cuid2({ message }): 有効なCUID2形式であるかチェック.ulid({ message }): 有効なULID形式であるかチェック.emoji({ message }): 絵文字のみで構成されているかチェック.datetime({ message, precision, offset }): ISO 8601形式の日付文字列であるかチェック (タイムゾーン情報など).ip({ version, message }): IPアドレス形式であるかチェック (v4 or v6).dateTime({ message, precision, offset }):.datetimeのエイリアス.date({ message }): YYYY-MM-DD形式の日付文字列であるかチェック.time({ message, precision }): HH:mm:ss[.sss]形式の時刻文字列であるかチェック.endsWith(suffix, { message }): 特定の文字列で終わるかチェック.startsWith(prefix, { message }): 特定の文字列で始まるかチェック.includes(value, { message }): 特定の文字列を含むかチェック.toLowerCase(): バリデーション後に小文字に変換(変換については後述).toUpperCase(): バリデーション後に大文字に変換.trim(): バリデーション後に前後の空白を除去.nonempty({ message }): 空文字列でないかチェック (.min(1))
z.number() には以下のメソッドがあります。
.lt(value, { message }): valueより小さい.lte(value, { message }): value以下.gt(value, { message }): valueより大きい.gte(value, { message }): value以上.min(value, { message }): value以上 (.gteのエイリアス).max(value, { message }): value以下 (.lteのエイリアス).finite({ message }): 無限大でない.int({ message }): 整数である.positive({ message }): 正の数である (.gt(0)).negative({ message }): 負の数である (.lt(0)).nonpositive({ message }): 0以下である (.lte(0)).nonnegative({ message }): 0以上である (.gte(0)).multipleOf(value, { message }): valueの倍数である.step(value, { message }): valueの倍数である (.multipleOfのエイリアス)
これらのメソッドはチェーンして記述できます。
“`typescript
import { z } from ‘zod’;
// 10文字以上20文字以下の文字列スキーマ
const usernameSchema = z.string().min(10, { message: “ユーザー名は10文字以上である必要があります” }).max(20, { message: “ユーザー名は20文字以下である必要があります” });
// 0以上100以下の整数スキーマ
const ageSchema = z.number().int({ message: “年齢は整数である必要があります” }).nonnegative({ message: “年齢は0以上である必要があります” }).max(100, { message: “年齢は100以下である必要があります” });
// 有効なメールアドレススキーマ
const emailSchema = z.string().email({ message: “無効なメールアドレス形式です” });
“`
オブジェクトスキーマ
Zodで最もよく使われるのは、オブジェクトの構造を定義する z.object() です。これは、オブジェクトが持つべきプロパティとその型を定義します。
“`typescript
import { z } from ‘zod’;
const userSchema = z.object({
id: z.string().uuid(), // UUID形式の文字列
name: z.string().min(1), // 1文字以上の文字列
age: z.number().int().positive(), // 正の整数
email: z.string().email().optional(), // オプションのメールアドレス
createdAt: z.date(), // Dateオブジェクト (またはバリデーション後のDateオブジェクトに変換される文字列)
});
// userSchemaに対応するTypeScriptの型は、z.inferを使って自動的に推論されます
type User = z.infer
/
type User = {
id: string;
name: string;
age: number;
email?: string | undefined;
createdAt: Date;
}
/
“`
オブジェクトスキーマのプロパティは、デフォルトで必須(required)です。プロパティをオプショナルにするには、そのスキーマに .optional() メソッドをチェーンします。これは、プロパティが存在しない場合 (undefined) や、値が undefined であることを許容します。
また、値が null であることを許容する場合は .nullable() メソッドを使います。
optional() と nullable() は組み合わせて使うこともできます (.optional().nullable() あるいは .nullable().optional())。これは、「プロパティが存在しないか、値が null または undefined であることを許容する」という意味になります。
typescript
const nullableString = z.string().nullable(); // string | null
const optionalNumber = z.number().optional(); // number | undefined
const optionalNullableBoolean = z.boolean().optional().nullable(); // boolean | undefined | null
オブジェクトスキーマには、他にも便利なメソッドがあります。
.partial(): オブジェクトの全てのプロパティをオプショナルにする。.deepPartial(): オブジェクトとそのネストされたオブジェクトの全てのプロパティを再帰的にオプショナルにする。.pick({ key1: true, key2: true }): オブジェクトから指定したプロパティのみを含む新しいスキーマを作成する。.omit({ key1: true }): オブジェクトから指定したプロパティを除外した新しいスキーマを作成する。.extend({ newKey: newSchema }): オブジェクトに新しいプロパティを追加した新しいスキーマを作成する。.merge(anotherObjectSchema): 他のオブジェクトスキーマとマージする。プロパティが重複する場合はマージする側のスキーマが優先される。
配列スキーマ
配列のスキーマは z.array() を使って定義します。引数には、配列の各要素のスキーマを指定します。
“`typescript
import { z } from ‘zod’;
// 数値の配列
const numbersArraySchema = z.array(z.number());
// ユーザーオブジェクトの配列
const usersArraySchema = z.array(userSchema);
// 配列にも制約を追加できます
const nonEmptyStringArray = z.array(z.string()).min(1, { message: “少なくとも1つの要素が必要です” });
const fixedLengthNumberArray = z.array(z.number()).length(3, { message: “要素は3つである必要があります” });
“`
配列スキーマには、以下のメソッドがあります。
.min(minLength, { message }): 最小要素数を指定.max(maxLength, { message }): 最大要素数を指定.length(length, { message }): 固定要素数を指定.nonempty({ message }): 少なくとも1つの要素があるかチェック (.min(1))
ユニオン(共用体)とインターセクション(交差型)
TypeScriptと同様に、Zodでもユニオン型(複数の型のいずれか)とインターセクション型(複数の型全てを満たす)を定義できます。
- ユニオン (union):
z.union([schema1, schema2, ...])
指定されたスキーマのいずれか1つにマッチする場合にバリデーションが成功します。 - インターセクション (intersection):
z.intersection(schema1, schema2)
指定された全てのスキーマにマッチする場合にバリデーションが成功します。主にオブジェクトスキーマを組み合わせて使う場合に有用です。
“`typescript
import { z } from ‘zod’;
// 文字列または数値
const stringOrNumber = z.union([z.string(), z.number()]);
type StringOrNumber = z.infer
// AdminUser または EditorUser
const AdminUserSchema = z.object({ role: z.literal(“admin”), adminId: z.string() });
const EditorUserSchema = z.object({ role: z.literal(“editor”), editorId: z.string() });
const UserRoleSchema = z.union([AdminUserSchema, EditorUserSchema]);
type UserRole = z.infer
// BaseUserとHasPermissionsを組み合わせたスキーマ
const BaseUserSchema = z.object({ id: z.string(), name: z.string() });
const HasPermissionsSchema = z.object({ permissions: z.array(z.string()) });
const UserWithPermissionsSchema = z.intersection(BaseUserSchema, HasPermissionsSchema);
type UserWithPermissions = z.infer
“`
ユニオン型は、特に異なる構造を持つ複数の可能性を扱いたい場合に非常に便利です。ただし、Zodのユニオンバリデーションは定義された順にスキーマを試行し、最初にマッチしたものを使用する点に注意が必要です。あいまいなユニオン(例: z.union([z.string(), z.string().min(5)]) のような、より具体的なスキーマが汎用的なスキーマに含まれてしまうケース)は避けるべきです。このような場合は、ディスクリミネートユニオン(後述)がより適しています。
タプルスキーマ
決まった数、決まった型の要素を持つ配列(タプル)を定義するには z.tuple() を使います。引数には、各要素のスキーマを配列で指定します。
“`typescript
import { z } from ‘zod’;
// [string, number, boolean] という構造のタプル
const stringNumberBooleanTuple = z.tuple([z.string(), z.number(), z.boolean()]);
type StringNumberBoolean = z.infer
// RGBカラー値のタプル ([number, number, number])
const rgbColorSchema = z.tuple([z.number().int().min(0).max(255), z.number().int().min(0).max(255), z.number().int().min(0).max(255)]);
“`
タプルスキーマは、要素の順序と数が厳密に決まっている場合に有用です。
バリデーションの実行とエラーハンドリング
Zodスキーマを定義したら、いよいよ実際のデータを検証します。検証には主に2つのメソッドがあります。
schema.parse(data: unknown): Tschema.safeParse(data: unknown): { success: true; data: T } | { success: false; error: ZodError }
parse() メソッド
parse() メソッドは、与えられたデータがスキーマに一致するかどうかを検証します。データが有効であれば、そのデータを返します(変換処理が定義されていれば、変換後のデータ)。データが無効な場合は、ZodError というエラーをスローします。
これは、データが有効であることが強く期待される場面や、無効なデータの場合は即座に処理を中断したい場面に適しています。
“`typescript
import { z, ZodError } from ‘zod’;
const userSchema = z.object({
name: z.string(),
age: z.number(),
});
const validData = { name: “Alice”, age: 30 };
const invalidData = { name: “Bob” }; // ageが不足
try {
const user = userSchema.parse(validData);
console.log(“Valid data:”, user); // { name: “Alice”, age: 30 } の型は z.infer
const invalidUser = userSchema.parse(invalidData); // ここで ZodError がスローされる
} catch (error) {
if (error instanceof ZodError) {
console.error(“Validation error:”, error.errors); // エラーの詳細情報を含む配列
/
[
{
“code”: “invalid_type”,
“expected”: “number”,
“received”: “undefined”,
“path”: [“age”],
“message”: “Required”
}
]
/
} else {
console.error(“Other error:”, error);
}
}
“`
parse() は、バリデーションが成功した場合に戻り値の型がスキーマに対応する型 (z.infer<typeof schema>) に絞り込まれるため、TypeScriptの型システムと非常に相性が良いです。
safeParse() メソッド
safeParse() メソッドもデータを検証しますが、無効な場合でもエラーをスローせず、代わりに結果をオブジェクトとして返します。結果オブジェクトは、バリデーションの成否を示す success プロパティと、成功時は data プロパティ、失敗時は error プロパティを持ちます。
これは、バリデーションの成否に基づいて異なる処理を行いたい場面や、エラーハンドリングをより明示的に行いたい場面に適しています。APIエンドポイントでのリクエストボディのバリデーションなど、入力が無効である可能性が高い場面でよく使われます。
“`typescript
import { z } from ‘zod’;
const userSchema = z.object({
name: z.string(),
age: z.number(),
});
const validData = { name: “Alice”, age: 30 };
const invalidData = { name: “Bob” }; // ageが不足
const result1 = userSchema.safeParse(validData);
if (result1.success) {
console.log(“Validation successful:”, result1.data); // { name: “Alice”, age: 30 } の型は z.infer
} else {
console.error(“Validation failed:”, result1.error.errors);
}
const result2 = userSchema.safeParse(invalidData);
if (result2.success) {
console.log(“Validation successful:”, result2.data);
} else {
console.error(“Validation failed:”, result2.error.errors); // エラーの詳細情報
}
“`
safeParse() はエラーをキャッチする必要がないため、非同期処理(Promiseを返すスキーマなど)を含むバリデーションでも扱いやすいです。
ZodErrorとエラーメッセージのカスタマイズ
バリデーションが失敗した場合にスローされる ZodError オブジェクト、または safeParse の結果に含まれる error オブジェクトは、バリデーション失敗に関する詳細な情報を含んでいます。特に、error.errors プロパティは、バリデーションが失敗した各箇所に関する情報の配列です。各エラーオブジェクトは、以下のプロパティを持つことが一般的です。
code: エラーの種類を示すコード (例: “invalid_type”, “too_small”, “invalid_string”)message: エラーメッセージpath: エラーが発生したデータのパス(例:['user', 'address', 'zipCode'])expected: 期待される型や値(場合による)received: 実際に受け取った型や値(場合による)
これらの情報を使って、ユーザーフレンドリーなエラーメッセージを表示したり、エラーの原因を特定したりできます。
デフォルトのエラーメッセージは英語ですが、メッセージをカスタマイズする方法がいくつかあります。
-
スキーマメソッドのオプションとして指定: 多くの制約メソッド(
.min(),.email(),.object()など)は、第二引数に{ message: "カスタムメッセージ" }というオブジェクトを受け取ります。typescript
z.string().min(5, { message: "最低5文字必要です" });
z.object({ name: z.string() }, { message: "無効なデータ形式です" }); // オブジェクト自体のエラーメッセージ
2. グローバルなメッセージマップを設定:z.setErrorMap()を使うと、エラーコードに対応するデフォルトメッセージを上書きできます。これにより、アプリケーション全体で一貫したエラーメッセージを日本語などで表示できます。“`typescript
import { z } from ‘zod’;const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
if (issue.code === z.ZodIssueCode.invalid_type) {
return { message:${ctx.path.join(".")} の型が無効です。期待される型: ${issue.expected}, 受け取った型: ${issue.received}};
}
if (issue.code === z.ZodIssueCode.too_small && issue.kind === “string”) {
return { message:${ctx.path.join(".")} は最低 ${issue.minimum} 文字必要です。};
}
// 他のコードにも対応…
return { message: ctx.defaultError }; // デフォルトのメッセージを使う場合
};z.setErrorMap(customErrorMap);
const schema = z.string().min(5);
const result = schema.safeParse(“abc”);if (!result.success) {
console.log(result.error.errors[0].message); // ” の型が無効です。期待される型: string, 受け取った型: string” または ” は最低 5 文字必要です。” のようなカスタムメッセージが表示される
}
“`
このように、Zodは強力なバリデーション機能と柔軟なエラーハンドリングメカニズムを提供します。
TypeScriptとの連携:Zodスキーマからの型推論
Zodの最も強力な特徴は、TypeScriptの型システムとのシームレスな連携です。Zodで定義したスキーマから、対応するTypeScriptの型定義を自動的に推論できます。これにより、手動で型定義とスキーマ定義を同期させる手間が省け、両者の間に乖離が生じるリスクを根本的になくすことができます。
この型推論機能は、主に z.infer<typeof yourSchema> というユーティリティ型を使って実現されます。
“`typescript
import { z } from ‘zod’;
const userSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
age: z.number().int().positive(),
email: z.string().email().optional(),
roles: z.array(z.enum([“admin”, “editor”, “viewer”])),
createdAt: z.date().transform((val) => val.toISOString()), // transformについては後述
});
// ZodスキーマからTypeScriptの型を推論
type User = z.infer
/*
推論される型 User:
type User = {
id: string;
name: string;
age: number;
email?: string | undefined;
roles: (“admin” | “editor” | “viewer”)[];
createdAt: string; // <– transformによってstringになる
}
*/
// この型を使って変数を宣言したり、関数の引数/戻り値に型を付けたりできます
function processUser(user: User): void {
console.log(Processing user: ${user.name} (ID: ${user.id}));
if (user.email) {
console.log(Email: ${user.email});
}
console.log(Roles: ${user.roles.join(", ")});
console.log(Created At: ${user.createdAt});
}
// APIから取得した未知のデータをバリデーションし、型安全に扱う
async function fetchAndProcessUser(userId: string): Promise
const response = await fetch(/api/users/${userId});
const rawData = await response.json(); // rawDataの型は unknown
const result = userSchema.safeParse(rawData);
if (result.success) {
const user: User = result.data; // バリデーションが成功したので、型は User に絞り込まれる
processUser(user); // User 型なので安全にアクセスできる
} else {
console.error(“Failed to validate user data:”, result.error.errors);
// エラー処理…
}
}
“`
z.infer<typeof schema> は、スキーマが バリデーション後に生成する型 を推論します。これは特に .transform() メソッド(後述)を使った場合に重要になります。元の入力データの型 (z.input<typeof schema>) と、バリデーション・変換後の出力データの型 (z.output<typeof schema>, これは z.infer と同じです) を区別することで、より正確な型付けが可能になります。
型定義とスキーマ定義の一元化のメリット
- SSOT (Single Source of Truth): データの構造に関する真実の情報源がZodスキーマ一つに集約されます。型定義とバリデーションロジックが常に一致していることが保証されます。
- 開発効率の向上: 型定義を手動で記述・更新する必要がなくなります。スキーマを変更すれば、型も自動的に追随します。
- 保守性の向上: コードの変更箇所が減り、バグの混入リスクが低減します。
- 実行時と静的型チェックの相補性: TypeScriptの静的型チェックは開発段階で多くのエラーを発見しますが、実行時に初めて明らかになるデータの型や構造に関する問題はチェックできません。Zodの実行時バリデーションは、この静的型チェックの盲点を補い、より堅牢なアプリケーションを実現します。
ZodとTypeScriptの連携は、従来のバリデーションライブラリにはない大きなアドバンテージであり、モダンなTypeScript開発においてZodが広く採用されている理由の一つです。
高度なスキーマ定義と機能
Zodは基本的な型や構造だけでなく、より複雑なシナリオに対応するための高度なスキーマタイプや機能も提供しています。
リテラル型とEnum
-
リテラル (literal):
z.literal(value)
特定の単一の値のみを許可するスキーマです。文字列、数値、真偽値、nullなどが指定できます。TypeScriptのリテラル型 ("admin",42,trueなど) に対応します。typescript
const adminRole = z.literal("admin");
type AdminRole = z.infer<typeof adminRole>; // "admin"
* Enum (enum):z.enum([...values])
指定された文字列リテラルのいずれか1つのみを許可するスキーマです。TypeScriptの文字列リテラルユニオン型 ("admin" | "editor" | "viewer") に対応します。JavaScriptのenumオブジェクトやTypeScriptのenumからも生成できます。“`typescript
const userRoles = z.enum([“admin”, “editor”, “viewer”]);
type UserRole = z.infer; // “admin” | “editor” | “viewer” // TypeScript enum から生成
enum Status {
PENDING = “PENDING”,
PROCESSING = “PROCESSING”,
COMPLETED = “COMPLETED”,
}
const statusSchema = z.nativeEnum(Status);
type StatusType = z.infer; // Status.PENDING | Status.PROCESSING | Status.COMPLETED
“`
ディスクリミネートユニオン (Discriminated Union)
ディスクリミネートユニオンは、ユニオン型の中でも、特定のプロパティ(判別プロパティ、Discriminator)の値によってユニオンのどのメンバーであるかを識別できる構造です。Zodでは z.discriminatedUnion(discriminatorKey, [unionSchemas]) を使って定義します。これは、異なる構造を持つオブジェクトのユニオンを型安全に扱う上で非常に強力です。
“`typescript
import { z } from ‘zod’;
const CatSchema = z.object({
animalType: z.literal(“cat”), // 判別プロパティ
meow: z.string(),
});
const DogSchema = z.object({
animalType: z.literal(“dog”), // 判別プロパティ
bark: z.string(),
breed: z.string(),
});
const AnimalSchema = z.discriminatedUnion(“animalType”, [CatSchema, DogSchema]);
type Animal = z.infer
/
type Animal =
{ animalType: “cat”; meow: string; } |
{ animalType: “dog”; bark: string; breed: string; }
/
const data1 = { animalType: “cat”, meow: “mew!” };
const data2 = { animalType: “dog”, bark: “woof!”, breed: “Golden Retriever” };
const data3 = { animalType: “cat”, bark: “woof!” }; // 無効なデータ
const animal1 = AnimalSchema.parse(data1); // 型は CatSchema に絞り込まれる
console.log(animal1.meow); // 安全にアクセスできる
const animal2 = AnimalSchema.parse(data2); // 型は DogSchema に絞り込まれる
console.log(animal2.breed); // 安全にアクセスできる
const result3 = AnimalSchema.safeParse(data3);
console.log(result3.success); // false
“`
ディスクリミネートユニオンは、パターンマッチングや条件分岐を使って、ユニオンのメンバーごとに異なる処理を行う場合に特に役立ちます。ZodとTypeScriptの組み合わせにより、実行時のデータ構造の検証と、開発時の型安全なコード記述が両立します。
マップとセット (Map and Set)
Zodは Map と Set コレクションにも対応しています。
-
Map:
z.map(keySchema, valueSchema)
キーと値それぞれのスキーマを指定します。typescript
const stringNumberMap = z.map(z.string(), z.number());
type StringNumberMap = z.infer<typeof stringNumberMap>; // Map<string, number>
* Set:z.set(elementSchema)
セットの各要素のスキーマを指定します。typescript
const numberSet = z.set(z.number());
type NumberSet = z.infer<typeof numberSet>; // Set<number>
関数、プロミス、その他
Zodはさらに多くのスキーマタイプをサポートしています。
-
関数 (function):
z.function(argsSchema, returnSchema)
関数の引数と戻り値の型を定義します。関数のシグネチャをバリデーションできます。“`typescript
const greetFn = z.function(
z.tuple([z.string()]), // 引数: [string]
z.string() // 戻り値: string
);
type GreetFn = z.infer; // (args: [string]) => string // このスキーマは関数オブジェクト自体をバリデーションします。
// 関数の呼び出し時に引数や戻り値をバリデーションするものではありません。
// 関数呼び出しのバリデーションには引数/戻り値のスキーマを別途使用します。
``z.promise(valueSchema)`
* **プロミス (promise):**
Promiseが解決された値のスキーマを指定します。typescript
const numberPromise = z.promise(z.number());
type NumberPromise = z.infer<typeof numberPromise>; // Promise<number>
* インスタンス (instanceof):z.instanceof(Class)
特定のクラスのインスタンスであることをチェックします。typescript
class Person { name: string; constructor(name: string) { this.name = name; } }
const personInstance = z.instanceof(Person);
type PersonInstance = z.infer<typeof personInstance>; // Person
* 未知 (unknown):z.unknown()
どんな値でも受け入れますが、その後のアクセスには型アサーションやバリデーションが必要です。anyより安全です。
* バイナリデータ (Uint8Array):z.instanceof(Uint8Array)などを使って表現できます。
再帰的なスキーマ (Recursive Schemas)
ツリー構造やリンクリストのように、自分自身を含む構造を持つデータを扱う場合、再帰的なスキーマ定義が必要になります。Zodでは z.lazy(() => schema) を使ってこれを実現します。z.lazy は、評価が遅延されるスキーマを定義し、循環参照を防ぎます。
“`typescript
import { z } from ‘zod’;
// NodeSchema は NodeSchema 自身を参照する可能性があるため、z.lazy で囲む
const NodeSchema: z.ZodType
z.object({
value: z.string(),
children: z.array(NodeSchema), // ここで再帰的に NodeSchema を参照
})
);
type Node = {
value: string;
children: Node[];
};
const treeData = {
value: “root”,
children: [
{
value: “child1”,
children: [
{ value: “grandchild1”, children: [] }
]
},
{
value: “child2”,
children: []
}
]
};
const validTree = NodeSchema.parse(treeData);
console.log(validTree);
“`
カスタムバリデーション (.refine と .superRefine)
Zodの組み込みバリデーションでは表現できない複雑なルールをチェックしたい場合は、.refine() または .superRefine() を使ってカスタムバリデーションロジックを追加できます。
-
.refine(predicate, params)
単一の条件(predicate関数)でチェックします。条件が満たされない場合、単一のエラーを生成します。predicate関数は検証対象のデータを受け取り、真偽値を返します。paramsオブジェクトでエラーメッセージなどを指定できます。“`typescript
const passwordSchema = z.string().min(8).refine(password => {
// パスワードに数字と大文字を含むかチェック
return /[0-9]/.test(password) && /[A-Z]/.test(password);
}, {
message: “パスワードは数字と大文字をそれぞれ1つ以上含む必要があります”
});passwordSchema.safeParse(“password”); // fails
passwordSchema.safeParse(“Password123”); // success
``.superRefine((data, ctx) => { … })
*複数の条件をチェックしたり、より詳細なエラー情報を追加したりする場合に使います。predicate関数は検証対象のデータとコンテキストオブジェクト (ctx) を受け取ります。条件が満たされない場合、ctx.addIssue()を呼び出してエラーを追加します。addIssueはZodIssue` オブジェクト(またはその一部)を受け取り、エラーコード、メッセージ、パスなどを細かく指定できます。“`typescript
const dateRangeSchema = z.object({
start: z.date(),
end: z.date(),
}).superRefine((data, ctx) => {
// 開始日が終了日より前であることをチェック
if (data.start >= data.end) {
ctx.addIssue({
code: z.ZodIssueCode.custom, // カスタムエラーコード
message: “開始日は終了日より前である必要があります”,
path: [“start”], // エラーが発生したパスを指定
});
ctx.addIssue({ // 終了日にもエラーを追加することも可能
code: z.ZodIssueCode.custom,
message: “終了日は開始日より後である必要があります”,
path: [“end”],
});
}
});dateRangeSchema.safeParse({ start: new Date(‘2023-01-01’), end: new Date(‘2023-01-15’) }); // success
dateRangeSchema.safeParse({ start: new Date(‘2023-01-15’), end: new Date(‘2023-01-01’) }); // fails with custom errors on start and end
``.superRefine` は複数のエラーを追加できるため、フォームバリデーションなどでユーザーに一度に複数の入力エラーを伝えたい場合に特に有用です。
データ変換 (.transform)
Zodスキーマは単にデータを検証するだけでなく、バリデーションが成功したデータに対して変換処理を適用することもできます。これは .transform() メソッドを使って行います。
schema.transform((value, ctx) => { ... }) は、検証対象のデータを受け取り、変換後の値を返します。変換はバリデーション成功後にのみ実行されます。変換後のデータの型が、そのスキーマの z.infer 型になります。
“`typescript
import { z } from ‘zod’;
// 文字列の数字を数値に変換
const numberFromStringSchema = z.string().refine(val => !isNaN(parseFloat(val)), { message: “有効な数値文字列ではありません” })
.transform(val => parseFloat(val));
type NumberFromString = z.infer
console.log(numberFromStringSchema.parse(“123”)); // 123 (number)
// 日付文字列をDateオブジェクトに変換
const dateFromStringSchema = z.string().datetime({ message: “有効な日付文字列ではありません” })
.transform(val => new Date(val));
type DateFromString = z.infer
console.log(dateFromStringSchema.parse(“2023-01-01T10:00:00Z”)); // Date オブジェクト
// オブジェクト変換: 特定のフィールドを整形
const userSchemaWithTransform = z.object({
firstName: z.string(),
lastName: z.string(),
}).transform(data => ({
fullName: ${data.firstName} ${data.lastName},
initials: ${data.firstName[0]}${data.lastName[0]},
}));
type UserWithTransform = z.infer
/
type UserWithTransform = {
fullName: string;
initials: string;
}
/
const transformedUser = userSchemaWithTransform.parse({ firstName: “Alice”, lastName: “Smith” });
console.log(transformedUser); // { fullName: “Alice Smith”, initials: “AS” }
“`
.transform は、APIから受け取ったデータをアプリケーション内部で扱いやすい形式に変換したり、環境変数などの設定値を適切な型にパースしたりするのに非常に役立ちます。.transform はチェーンして複数適用することも可能です。
ブランド型 (.brand)
z.brand<Brand> は、Zodのスキーマに「ブランド」(識別子)を付けることで、同じ基本型を持つ異なる概念をTypeScriptの型システム上で区別できるようにする機能です。これは特に、特定の形式や検証済みであることを型レベルで表現したい場合に有用です。
“`typescript
import { z } from ‘zod’;
// Email というブランドを持つ string 型
type EmailBrand = { readonly Email: unique symbol };
const EmailSchema = z.string().email().brand
// Password というブランドを持つ string 型
type PasswordBrand = { readonly Password: unique symbol };
const PasswordSchema = z.string().min(8).brand
type Email = z.infer
type Password = z.infer
function sendEmail(to: Email, subject: string, body: string) {
// to は必ず検証済みのメールアドレス(Email型)であると型システムが保証
console.log(Sending email to ${to});
}
const validEmail = EmailSchema.parse(“[email protected]”);
const plainString = “[email protected]”;
sendEmail(validEmail, “Hello”, “…”); // OK
// sendEmail(plainString, “Hello”, “…”); // 型エラー! string は Email 型に代入できない
const validPassword = PasswordSchema.parse(“SecurePassword123”);
const anotherString = “short”;
// const user = { email: validEmail, password: validPassword }; // OK
// const invalidUser = { email: validEmail, password: anotherString }; // 型エラー!
“`
z.brand を使うことで、単なる string ではなく「検証済みのメールアドレス文字列」や「安全性が保証されたパスワード文字列」といった、より意味合いの強い型を定義し、関数シグネチャなどでそれらの型を要求することで、開発時の型安全性をさらに高めることができます。ブランド情報は実行時には存在しない(Erasureされる)ため、パフォーマンスへの影響はありません。
実践的なZodの利用例
Zodはその柔軟性とTypeScript連携の強みから、様々な場面で活用できます。主要な利用例をいくつか見ていきましょう。
APIエンドポイントでのリクエスト/レスポンスバリデーション
Webアプリケーション開発において、APIエンドポイントは外部からのデータ(リクエストボディ、クエリパラメータ、ヘッダーなど)を受け取る主要な入り口です。これらのデータは常に信頼できるとは限らないため、サーバーサイドで厳格なバリデーションを行うことはセキュリティと安定性の観点から必須です。Zodはここで非常に強力な役割を果たします。
サーバーサイド (Node.js/Expressの例):
Expressでは、カスタムミドルウェアとしてZodバリデーションを組み込むのが一般的です。
“`typescript
import express, { Request, Response, NextFunction } from ‘express’;
import { z, ZodError } from ‘zod’;
// リクエストボディのスキーマを定義
const createUserSchema = z.object({
body: z.object({
name: z.string().min(1, “名前は必須です”),
email: z.string().email(“有効なメールアドレスを入力してください”),
password: z.string().min(8, “パスワードは8文字以上である必要があります”),
age: z.number().int().positive().optional(),
})
});
// スキーマに基づいてリクエストをバリデーションするミドルウェアファクトリ
const validate = (schema: any) => (req: Request, res: Response, next: NextFunction) => {
try {
// req.body, req.query, req.params などをまとめてバリデーション
schema.parse({
body: req.body,
query: req.query,
params: req.params,
});
next(); // バリデーション成功
} catch (error) {
if (error instanceof ZodError) {
// バリデーション失敗
res.status(400).json({ errors: error.errors });
} else {
// その他のエラー
res.status(500).json({ message: “Internal Server Error” });
}
}
};
const app = express();
app.use(express.json()); // JSONボディをパース
// /users エンドポイントでのPOSTリクエストにバリデーションミドルウェアを適用
app.post(‘/users’, validate(createUserSchema), (req, res) => {
// ここに到達する req.body は createUserSchema.shape.body の型安全なオブジェクトです
const userData = req.body; // 型は z.infer
console.log(“Valid user data received:”, userData);
// データベース保存などの処理…
res.status(201).json({ message: “User created”, data: userData });
});
// …他のルート定義…
app.listen(3000, () => {
console.log(‘Server is running on port 3000’);
});
“`
このアプローチの利点は、バリデーションロジックがエンドポイントハンドラーから分離され、再利用可能なミドルウェアとして定義できる点です。また、バリデーション後の req.body などのデータがZodによって型安全であることが保証されるため、その後の処理を安心して記述できます。
他のフレームワーク(Koa, Honoなど)でも同様のアプローチでZodバリデーションを組み込むことができます。特にHonoはZodとの連携が非常に強力で、ルーティング定義の中でZodスキーマを使ってリクエストやレスポンスの型を定義し、自動的なバリデーションと型推論を活用できます。tRPCのようなエンドツーエンドの型安全なRPCフレームワークも、内部でZod(またはそれに類するもの)を使ってスキーマ定義と型推論を行っています。
クライアントサイド (APIレスポンスのバリデーション):
クライアントサイドでも、APIから受け取ったレスポンスデータが期待通りの構造や型であるかを検証することは重要です。悪意のある、あるいはバグのあるAPIからのレスポンスによって、クライアントアプリケーションがクラッシュしたり、不正な状態になったりするのを防ぐことができます。
“`typescript
import { z, ZodError } from ‘zod’;
// APIレスポンスデータのスキーマを定義
const UserApiResponseSchema = z.object({
id: z.string().uuid(),
name: z.string(),
isActive: z.boolean(),
createdAt: z.string().datetime(), // APIからは文字列で来ると想定
});
type UserApiResponse = z.infer
async function fetchUser(userId: string): Promise
try {
const response = await fetch(/api/users/${userId});
if (!response.ok) {
console.error(“API error:”, response.statusText);
return null;
}
const rawData: unknown = await response.json(); // 未知のデータとして受け取る
// 受け取ったデータをバリデーション
const validationResult = UserApiResponseSchema.safeParse(rawData);
if (validationResult.success) {
console.log("Response data is valid.");
const userData: UserApiResponse = validationResult.data;
return userData; // 型安全なデータを返す
} else {
console.error("API response validation failed:", validationResult.error.errors);
// 無効なデータに対するクライアントサイドのフォールバック処理などを記述
return null;
}
} catch (error) {
console.error(“Error fetching user:”, error);
return null;
}
}
// 使用例
fetchUser(“some-user-id”).then(user => {
if (user) {
// user は型安全な UserApiResponse 型
console.log(Fetched user: ${user.name}, Active: ${user.isActive});
} else {
console.log(“Failed to fetch or validate user data.”);
}
});
“`
クライアントサイドでのAPIレスポンスバリデーションは、堅牢なフロントエンドを構築する上で見落とされがちなステップですが、Zodを使うことでこれを容易かつ型安全に行うことができます。
フォームバリデーション
Webフォームの入力値バリデーションは、Zodのもう一つの一般的な利用シーンです。クライアントサイドでの即時フィードバック(UX向上)と、サーバーサイドでの最終的な検証(セキュリティとデータの信頼性)の両方でZodを活用できます。
特にReact開発においては、react-hook-form や formik といったフォームライブラリがZodバリデーションをサポートしており、非常に効率的に型安全なフォームを構築できます。
React Hook Form と Zod の連携例:
“`typescript
import React from ‘react’;
import { useForm } from ‘react-hook-form’;
import { zodResolver } from ‘@hookform/resolvers/zod’; // zod 用のリゾルバー
import { z } from ‘zod’;
// フォームデータのスキーマを定義
const ContactFormSchema = z.object({
name: z.string().min(1, “名前は必須です”),
email: z.string().email(“有効なメールアドレスを入力してください”),
message: z.string().min(10, “メッセージは10文字以上である必要があります”).max(500, “メッセージは500文字以下である必要があります”),
});
// スキーマからフォームデータの型を推論
type ContactFormData = z.infer
function ContactForm() {
const { register, handleSubmit, formState: { errors } } = useForm
// Zodスキーマをバリデーションリゾルバーとして指定
resolver: zodResolver(ContactFormSchema),
});
const onSubmit = (data: ContactFormData) => {
// ここに到達する data は ContactFormData 型で、Zodによって検証済み
console.log(“Form data is valid:”, data);
// API送信などの処理…
};
return (