この記事では、TypeScriptのスキーマ宣言とバリデーションのためのライブラリであるZodで最近躓いたりしたので、改めて実際にコードを動かしながら整理してみました。
軽く自己紹介
初めまして、ブルと申します。
元々アプリをメインに書いていたのですが、最近フロントエンドを書き始めてアドベントカレンダーの季節でもあったので改めてここでZodを整理してみました。
はじめに
Zodは、TypeScript向けのスキーマ宣言とバリデーションライブラリで、スキーマ(単純な文字列から複雑なネストオブジェクトまでのデータ構造)を簡潔に定義し、型安全を保ちながらバリデーションを行えます。
Zodの主な目的は、型安全なバリデーションを提供し、開発者が定義したスキーマに基づいて正確なデータ検証を可能にすることです。
スキーマの定義と基本的な使い方
Zodを使用して、データのスキーマを定義し、それに基づいてバリデーションを行います。
import { z } from 'zod';
// ユーザースキーマの定義
const userSchema = z.object({
name: z.string(),
age: z.number(),
email: z.string().email(),
});
// バリデーションするデータ
const userData = {
name: '太郎',
age: 30,
email: 'taro@example.com',
};
// バリデーションの実行
try {
const validatedData = userSchema.parse(userData);
console.log('バリデーション成功:', validatedData);
} catch (e) {
console.error('バリデーションエラー:', e.errors);
}
出力結果:
バリデーション成功: { name: '太郎', age: 30, email: 'taro@example.com' }
userSchema
でユーザーオブジェクトのスキーマを定義し、parse
メソッドでデータをバリデーションしています。
成功すると、バリデートされたデータが取得できます。
バリデーションエラーの処理
不正なデータをバリデーションすると、エラー情報が取得できます。
const invalidUserData = {
name: '花子',
age: '二十歳', // 不正な型(stringではなくnumberが期待される)
email: 'invalid-email', // 不正なメールアドレス形式
};
try {
const validatedData = userSchema.parse(invalidUserData);
console.log('バリデーション成功:', validatedData);
} catch (e) {
console.error('バリデーションエラー:', e.errors);
}
出力結果:
バリデーションエラー: [
{
code: 'invalid_type',
expected: 'number',
received: 'string',
path: ['age'],
message: 'Expected number, received string',
},
{
validation: 'email',
code: 'invalid_string',
message: 'Invalid email',
path: ['email'],
},
]
型の推論とz.infer
の活用
ZodのスキーマからTypeScriptの型を推論するには、z.infer
を使用します。
type User = z.infer<typeof userSchema>;
const newUser: User = {
name: '次郎',
age: 25,
email: 'jiro@example.com',
};
z.infer<typeof userSchema>
を使うことで、userSchema
に基づいたUser
型を生成できます。
z.infer
のメリット
- 型定義とバリデーションロジックが同じ1つのソースから作られるので、不整合を防げます
- 型の重複定義を避けられたりします
スキーマの拡張とextend
の活用
既存のスキーマに新しいフィールドを追加する場合、extend
メソッドが便利です。
const addressSchema = z.object({
street: z.string(),
city: z.string(),
zipCode: z.string().length(7),
});
// ユーザースキーマを拡張
const extendedUserSchema = userSchema.extend({
address: addressSchema,
});
// バリデーションするデータ
const extendedUserData = {
name: '太郎',
age: 30,
email: 'taro@example.com',
address: {
street: '中央通り1-1-1',
city: '東京都',
zipCode: '1234567',
},
};
try {
const validatedData = extendedUserSchema.parse(extendedUserData);
console.log('バリデーション成功:', validatedData);
} catch (e) {
console.error('バリデーションエラー:', e.errors);
}
出力結果:
バリデーション成功: {
name: '太郎',
age: 30,
email: 'taro@example.com',
address: { street: '中央通り1-1-1', city: '東京都', zipCode: '1234567' }
}
extend
のメリット
- 既存のスキーマをそのまま活用したいケースで便利です
- 変更があった場合でも、一箇所の修正で済みます
オプショナルなフィールドとデフォルト値
Zodでは、フィールドをオプショナルにしたり、デフォルト値を設定することができます。
const optionalUserSchema = z.object({
name: z.string(),
nickname: z.string().optional(),
age: z.number().default(20),
});
// データ例
const userData1 = { name: '太郎', nickname: 'タロちゃん' };
const userData2 = { name: '花子' };
try {
console.log('ユーザー1:', optionalUserSchema.parse(userData1));
console.log('ユーザー2:', optionalUserSchema.parse(userData2));
} catch (e) {
console.error('バリデーションエラー:', e.errors);
}
出力結果:
ユーザー1: { name: '太郎', nickname: 'タロちゃん', age: 20 }
ユーザー2: { name: '花子', age: 20 }
詳細
-
optional()
: フィールドを省略可能にします。 -
default()
: フィールドが存在しない場合、デフォルト値を設定します。
カスタムバリデーションとrefine
独自のバリデーションロジックを追加したい場合、refine
メソッドを使用します。
const passwordSchema = z.string().min(8).refine((val) => /[A-Z]/.test(val), {
message: 'パスワードには少なくとも一つの大文字が必要です。',
});
const passwords = ['password', 'Password123'];
passwords.forEach((pwd) => {
try {
passwordSchema.parse(pwd);
console.log(`パスワード "${pwd}" は有効です。`);
} catch (e) {
console.error(`パスワード "${pwd}" は無効です。`, e.errors);
}
});
出力結果:
パスワード "password" は無効です。 [
{
code: 'custom',
message: 'パスワードには少なくとも一つの大文字が必要です。',
path: [],
},
]
パスワード "Password123" は有効です。
refine
の活用方法
- 正規表現や複数の条件を組み合わせたバリデーションを扱いたい時
- 柔軟にバリデーションの文言を変えたい時
React Hook Formとの統合
フロントエンド開発でフォームバリデーションを行う際、React Hook FormとZodを組み合わせると効果的です。
必要なパッケージのインストール
npm install react-hook-form @hookform/resolvers
フォームスキーマの定義
import { z } from 'zod';
const formSchema = z.object({
username: z.string().min(1, { message: 'ユーザー名は必須です。' }),
email: z.string().email({ message: '有効なメールアドレスを入力してください。' }),
password: z.string().min(8, { message: 'パスワードは8文字以上である必要があります。' }),
});
type FormValues = z.infer<typeof formSchema>;
フォームコンポーネントの実装
import React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
const App: React.FC = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(formSchema),
});
const onSubmit = (data: FormValues) => {
console.log('フォームデータ:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>ユーザー名:</label>
<input {...register('username')} />
{errors.username && <p>{errors.username.message}</p>}
</div>
<div>
<label>メールアドレス:</label>
<input {...register('email')} />
{errors.email && <p>{errors.email.message}</p>}
</div>
<div>
<label>パスワード:</label>
<input type="password" {...register('password')} />
{errors.password && <p>{errors.password.message}</p>}
</div>
<button type="submit">送信</button>
</form>
);
};
export default App;
フォームの動作確認
- フォームを送信すると、Zodによるバリデーションが実行されます。
-
errors
オブジェクトから取得したエラーメッセージを表示します。
Zodのその他色々
z.string()
のメソッド
emoji()
文字列が全て絵文字で構成されているかを検証します。
const emojiSchema = z.string().emoji();
const testStrings = ['😊👍', 'Hello', 'こんにちは😊'];
testStrings.forEach((str) => {
try {
emojiSchema.parse(str);
console.log(`"${str}" は全て絵文字です。`);
} catch (e) {
console.error(`"${str}" は絵文字のみではありません。`);
}
});
出力結果:
"😊👍" は全て絵文字です。
"Hello" は絵文字のみではありません。
"こんにちは😊" は絵文字のみではありません。
ip()
IPアドレスのバリデーションが可能です。
const ipSchema = z.string().ip();
const ips = ['192.168.1.1', '2001:0db8:85a3:0000:0000:8a2e:0370:7334', 'invalid-ip'];
ips.forEach((ip) => {
try {
ipSchema.parse(ip);
console.log(`"${ip}" は有効なIPアドレスです。`);
} catch (e) {
console.error(`"${ip}" は無効なIPアドレスです。`);
}
});
出力結果:
"192.168.1.1" は有効なIPアドレスです。
"2001:0db8:85a3:0000:0000:8a2e:0370:7334" は有効なIPアドレスです。
"invalid-ip" は無効なIPアドレスです。
z.bigint()
の拡張
以下はz.bigint()
に数値型と同様の比較メソッドの例です。
const bigIntSchema = z.bigint().gt(BigInt(100));
const bigInts = [BigInt(200), BigInt(50)];
bigInts.forEach((num) => {
try {
bigIntSchema.parse(num);
console.log(`${num} は100より大きいです。`);
} catch (e) {
console.error(`${num} は100以下です。`);
}
});
出力結果:
200n は100より大きいです。
50n は100以下です。
まとめ
TypeScriptを書く中で時々遭遇するZodについて、改めて整理してみました。
今回はメリットに焦点を当てていますが、今後はアンチパターンについても考察してみたいと思います。
Zodを活用すれば、型定義と実行時バリデーションを一元化することで、開発効率を向上させる可能性がありますし、z.infer
やextend
を活用することで、型定義とスキーマを常に同期させられるメリットがあります。
また、公式にもある通り、React Hook Formと組み合わせれば、フロントエンドのフォームバリデーションがより効率的に行えるとのことなので、ぜひ活用してみてください!