TypeScriptのZodバリデーションライブラリ完全ガイド
TypeScriptプロジェクトにおけるZodを使ったスキーマバリデーションの基本から実践的な活用法まで詳しく解説します。
約5分で読めます
技術記事
実践的
この記事のポイント
TypeScriptプロジェクトにおけるZodを使ったスキーマバリデーションの基本から実践的な活用法まで詳しく解説します。
この記事では、実践的なアプローチで技術的な課題を解決する方法を詳しく解説します。具体的なコード例とともに、ベストプラクティスを学ぶことができます。
Zodは、TypeScriptファーストなスキーマバリデーションライブラリです。型安全性を保ちながら実行時バリデーションを行えるため、フロントエンドからバックエンドまで幅広く活用されています。
Zodとは
Zodは以下の特徴を持つバリデーションライブラリです:
- TypeScript ファースト: TypeScriptの型システムと完全に統合
- ゼロ依存: 外部ライブラリに依存しない軽量設計
- チェーン可能なAPI: 直感的でReadableなスキーマ定義
- 型推論: スキーマから自動的にTypeScript型を生成
基本的なセットアップ
インストール
npm install zod
# または
yarn add zod
基本的な使用法
import { z } from 'zod';
// スキーマ定義
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
age: z.number().min(0).max(120),
});
// 型の推論
type User = z.infer<typeof UserSchema>;
// バリデーション実行
const userData = {
id: 1,
name: "田中太郎",
email: "tanaka@example.com",
age: 30,
};
try {
const validUser = UserSchema.parse(userData);
console.log(validUser); // 型安全なデータ
} catch (error) {
console.error(error.issues); // バリデーションエラー詳細
}
基本的なスキーマタイプ
プリミティブ型
import { z } from 'zod';
// 文字列
const stringSchema = z.string();
const nonEmptyString = z.string().min(1);
const emailSchema = z.string().email();
const urlSchema = z.string().url();
// 数値
const numberSchema = z.number();
const positiveNumber = z.number().positive();
const integerSchema = z.number().int();
// 真偽値
const booleanSchema = z.boolean();
// 日付
const dateSchema = z.date();
const futureDate = z.date().min(new Date());
複合型
// 配列
const stringArraySchema = z.array(z.string());
const numberArraySchema = z.array(z.number()).min(1).max(10);
// オブジェクト
const PersonSchema = z.object({
name: z.string(),
age: z.number().optional(),
hobbies: z.array(z.string()),
});
// ユニオン型
const StatusSchema = z.union([
z.literal('pending'),
z.literal('approved'),
z.literal('rejected'),
]);
// Enum
const ColorSchema = z.enum(['red', 'green', 'blue']);
実践的なバリデーション例
APIレスポンスの検証
const ApiResponseSchema = z.object({
success: z.boolean(),
data: z.object({
users: z.array(z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
createdAt: z.string().datetime(),
})),
total: z.number(),
page: z.number(),
}),
error: z.string().nullable(),
});
async function fetchUsers(page: number) {
const response = await fetch(`/api/users?page=${page}`);
const rawData = await response.json();
// APIレスポンスを検証
const result = ApiResponseSchema.safeParse(rawData);
if (!result.success) {
throw new Error('Invalid API response format');
}
return result.data; // 型安全なデータ
}
フォームバリデーション
const RegisterFormSchema = z.object({
username: z.string()
.min(3, { message: "ユーザー名は3文字以上で入力してください" })
.max(20, { message: "ユーザー名は20文字以下で入力してください" })
.regex(/^[a-zA-Z0-9_]+$/, { message: "英数字とアンダースコアのみ使用可能です" }),
email: z.string()
.email({ message: "有効なメールアドレスを入力してください" }),
password: z.string()
.min(8, { message: "パスワードは8文字以上で入力してください" })
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {
message: "大文字、小文字、数字を含む必要があります"
}),
confirmPassword: z.string(),
birthDate: z.string()
.datetime()
.refine((date) => {
const age = new Date().getFullYear() - new Date(date).getFullYear();
return age >= 18;
}, { message: "18歳以上である必要があります" }),
terms: z.boolean()
.refine((val) => val === true, {
message: "利用規約に同意してください"
}),
}).refine((data) => data.password === data.confirmPassword, {
message: "パスワードが一致しません",
path: ["confirmPassword"],
});
type RegisterForm = z.infer<typeof RegisterFormSchema>;
高度な機能
カスタムバリデーション
const CustomSchema = z.object({
phoneNumber: z.string().refine((val) => {
// 日本の電話番号形式をチェック
const phoneRegex = /^(\+81|0)\d{1,4}-?\d{1,4}-?\d{4}$/;
return phoneRegex.test(val);
}, {
message: "有効な電話番号を入力してください",
}),
uniqueEmail: z.string().email().refine(async (email) => {
// データベースでユニークチェック(非同期)
const exists = await checkEmailExists(email);
return !exists;
}, {
message: "このメールアドレスは既に使用されています",
}),
});
条件付きスキーマ
const ConditionalSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('personal'),
name: z.string(),
age: z.number(),
}),
z.object({
type: z.literal('business'),
companyName: z.string(),
taxId: z.string(),
}),
]);
// 使用例
const personalData = {
type: 'personal' as const,
name: '田中太郎',
age: 30,
};
const businessData = {
type: 'business' as const,
companyName: '株式会社サンプル',
taxId: '123456789',
};
データ変換
const TransformSchema = z.object({
price: z.string().transform((val) => parseFloat(val)),
tags: z.string().transform((val) => val.split(',').map(tag => tag.trim())),
isActive: z.string().transform((val) => val === 'true'),
createdAt: z.string().transform((val) => new Date(val)),
});
const input = {
price: "1299.99",
tags: "typescript, zod, validation",
isActive: "true",
createdAt: "2024-12-26T10:00:00Z",
};
const result = TransformSchema.parse(input);
// result: {
// price: 1299.99,
// tags: ['typescript', 'zod', 'validation'],
// isActive: true,
// createdAt: Date object
// }
フレームワーク連携
React Hook Formとの連携
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
const FormSchema = z.object({
name: z.string().min(1, "名前を入力してください"),
email: z.string().email("有効なメールアドレスを入力してください"),
});
type FormData = z.infer<typeof FormSchema>;
function MyForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(FormSchema),
});
const onSubmit = (data: FormData) => {
console.log(data); // 型安全なデータ
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} />
{errors.name && <span>{errors.name.message}</span>}
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<button type="submit">送信</button>
</form>
);
}
Next.js API Routesでの使用
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().min(0),
});
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const userData = CreateUserSchema.parse(body);
// データベース操作
const user = await createUser(userData);
return NextResponse.json({ success: true, user });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ success: false, errors: error.issues },
{ status: 400 }
);
}
return NextResponse.json(
{ success: false, message: 'Internal server error' },
{ status: 500 }
);
}
}
パフォーマンスとベストプラクティス
スキーマの再利用
// 基本スキーマ
const BaseUserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
// 拡張スキーマ
const CreateUserSchema = BaseUserSchema.omit({ id: true });
const UpdateUserSchema = BaseUserSchema.partial();
const UserWithTimestamps = BaseUserSchema.extend({
createdAt: z.date(),
updatedAt: z.date(),
});
エラーハンドリング
function validateUserData(data: unknown) {
const result = UserSchema.safeParse(data);
if (!result.success) {
// 詳細なエラー情報を取得
const errors = result.error.issues.map(issue => ({
field: issue.path.join('.'),
message: issue.message,
code: issue.code,
}));
return { success: false, errors };
}
return { success: true, data: result.data };
}
まとめ
Zodは TypeScript プロジェクトにおいて、型安全性を保ちながら実行時バリデーションを実現する優れたライブラリです。
主な利点:
- 🛡️ 型安全性: コンパイル時と実行時の両方で型安全性を保証
- 🚀 開発体験: 直感的なAPIと優れた型推論
- 🔧 柔軟性: カスタムバリデーションと変換機能
- 🎯 エコシステム: React Hook Form、tRPCなどとの優れた連携
フォームバリデーション、API通信、設定ファイルの検証など、様々な場面でZodを活用して、より堅牢なTypeScriptアプリケーションを構築しましょう。