🎄and factory.inc Advent Calendar 202415日目の記事です🎄
はじめに
サイトやアプリで、ユーザーが情報を入力する場面って結構多いですよね。
会員登録、ログイン、購入機能、お問い合わせフォーム...
そこで入力内容の形式を制限するバリデーションの実装がお手軽にできて、TypeScriptと相性の良さそうなライブラリがあるので取り上げようと思います。
zodとは
zod公式ドキュメント によると「TypeScriptファーストのスキーマ宣言および検証ライブラリ」とのこと。TypeScriptファーストと記載してますがJavaScriptでも動くそうです。
使い方
import { z } from "zod";
// スキーマ定義
const UserSchema = z.object({
// 1文字以上の文字列
name: z.string().min(1, '名前は必須です'),
// 0以上の数値
age: z.number().min(0, '年齢は0以上である必要があります'),
// メールアドレスの形式かどうか
email: z.string().email('有効なメールアドレスを入力してください'),
});
// 型定義を生成
type User = z.infer<typeof UserSchema>;
// 入力データ
const inputData: User = {
name: 'John',
age: 25,
email: 'sample@example.com',
};
// パース処理(バリデーション)
const result = UserSchema.safeParse(inputData);
// safeParseの返り値を確認するため成功/失敗時の両方ともresultを返すようにしています
if (result.success) {
console.log('バリデーション成功:', result);
} else {
console.log('バリデーション失敗:', result);
}
スキーマ定義
UserSchemaで扱う値(今回だとname, age, email)の形式を設定しています。
min()はstringなら最小文字数、number型なら最小値の指定になります。最大値の指定はmax()です。
型と文字数制限などの形式が同じ場所に書けるので、どんな値を扱うかパッと見わかりやすいです。
型の自動生成
z.infer<>では指定したスキーマから型を自動で生成してくれます。
type User = z.infer<typeof UserSchema>;
// ↓ 生成される型
// type User = {
// name: string;
// age: number;
// email: string;
// };
実際に値を受け取りUser型を当てているinputDataと、zodで定義したUserSchemaの構造を簡単に一致させることができます。
スキーマを変更したら自動で型が更新されるのもありがたいです。
下記のようにnumber型を期待しているageにstring型の値をセットすると、バリデーション対象の値が期待した型と異なるとエディタ上でエラーが出ます。
const inputData: User = {
name: 'John',
// エラー「型 'string' を型 'number' に割り当てることはできません。」
age: '20',
email: 'sample@example.com',
};
メールアドレスの形式
許容される例
:user@example.com
、user@sub.domain.com、
許容されない例
:user@-example.com
、 user@domain..com
、 user@domain.c
、あいうえお@domain.com
特定の特殊文字(< > ( ) [ ] \ , ; : @ " 空白)を含まない、ドメイン名の末尾は英字2文字以上が必要、連続したドットは許容しない等の指定があり、xx@xx.xx
のような形式を満たしているかをチェックしています。
パース処理
safeParse使用時
バリデーション成功時にはresult.success
にtrueと成功データdata
を、失敗時にはresult.success
にfalseとエラー情報を持つオブジェクトerror
が返されます。
バリデーション成功時
バリデーション成功: {
"success": true,
"data": {
"name": "John",
"age": 20,
"email": "sample@domain.com"
}
}
バリデーション失敗時
バリデーション失敗: {
"success": false,
"error": {
"issues": [
{
"code": "too_small",
"minimum": 1,
"type": "string",
"inclusive": true,
"exact": false,
"message": "名前は必須です",
"path": [
"name"
]
},
{
"code": "invalid_type",
"expected": "number",
"received": "string",
"path": [
"age"
],
"message": "Expected number, received string"
},
{
"validation": "email",
"code": "invalid_string",
"message": "有効なメールアドレスを入力してください",
"path": [
"email"
]
}
],
"name": "ZodError"
},
"_error": {
"issues": [
{
"code": "too_small",
"minimum": 1,
"type": "string",
"inclusive": true,
"exact": false,
"message": "名前は必須です",
"path": [
"name"
]
},
{
"code": "invalid_type",
"expected": "number",
"received": "string",
"path": [
"age"
],
"message": "Expected number, received string"
},
{
"validation": "email",
"code": "invalid_string",
"message": "有効なメールアドレスを入力してください",
"path": [
"email"
]
}
],
"name": "ZodError"
}
}
parse使用時
バリデーション成功時には入力内容が、失敗時にはコンソールにエラーが表示されます。
safeParseの場合は例外をスローしないため、返されるオブジェクト内にあるsuccessプロパティがfalseなら失敗時という判定にしていましたが、
parseの場合は失敗時に例外をスローするのでtry...catch
で結果を受け取れます。
// 省略
try {
console.log('バリデーション成功:', result);
} catch(error) {
console.log(error);
}
バリデーション成功時
バリデーション成功: {
"name": "John",
"age": 25,
"email": "user.aaa@domain.com"
}
バリデーション失敗時
index.mjs:587 Uncaught ZodError: [
{
"code": "too_small",
"minimum": 1,
"type": "string",
"inclusive": true,
"exact": false,
"message": "名前は必須です",
"path": [
"name"
]
},
{
"validation": "email",
"code": "invalid_string",
"message": "有効なメールアドレスを入力してください",
"path": [
"email"
]
}
]
まとめ
zodは定義したスキーマをTypeScriptの型として利用(=スキーマから型定義を自動生成)できるため、型の安全性を保ったバリデーションができます。
今回の例ではバリデーション対象は名前・年齢・メールアドレスの3つだけでしたが、その他にも日付や時刻、urlを検証するメソッドが用意されていますので、入力フォームの項目数が多くなっても自力でのバリデーション実装に比べると負担が軽くなるかもしれません。