Valibot
モジュラーで型安全なスキーマライブラリ。軽量性とTree Shakingに最適化されたバリデーション
Valibot
Valibotは、TypeScript向けのモジュラーで型安全なスキーマバリデーションライブラリです。Zodと同様の機能を提供しながら、軽量性とTree Shakingの最適化に特化しています。
特徴
- 超軽量: 依存関係なしでバンドルサイズを最大95%削減
- 型安全: TypeScriptファーストで完全な型推論をサポート
- モジュラー設計: 小さな独立した関数で構成され、Tree Shakingに最適化
- 高性能: 大規模で複雑なスキーマでも高速な処理
- 豊富なエコシステム: React、Vue、Svelte、NestJS、DrizzleORMなどをサポート
インストール
# npm
npm install valibot
# yarn
yarn add valibot
# pnpm
pnpm add valibot
基本的な使用方法
スキーマの定義
import * as v from 'valibot';
// 基本的なプリミティブ型
const StringSchema = v.string();
const NumberSchema = v.number();
const BooleanSchema = v.boolean();
// オブジェクトスキーマ
const UserSchema = v.object({
id: v.number(),
name: v.string(),
email: v.string(),
isActive: v.boolean()
});
// 配列スキーマ
const NumberArraySchema = v.array(v.number());
const UserArraySchema = v.array(UserSchema);
バリデーションの実行
// parse - エラーの場合は例外をスロー
try {
const result = v.parse(UserSchema, {
id: 1,
name: "田中太郎",
email: "[email protected]",
isActive: true
});
console.log(result); // バリデーション済みデータ
} catch (error) {
console.error(error);
}
// safeParse - 例外をスローしない
const result = v.safeParse(UserSchema, data);
if (result.success) {
console.log(result.output); // バリデーション済みデータ
} else {
console.error(result.issues); // エラー詳細
}
// is - 型ガード関数
if (v.is(UserSchema, data)) {
// dataはUser型として扱える
console.log(data.name);
}
パイプラインバリデーション
import * as v from 'valibot';
// パイプラインを使用した高度なバリデーション
const EmailSchema = v.pipe(
v.string(),
v.email('有効なメールアドレスを入力してください')
);
const PasswordSchema = v.pipe(
v.string(),
v.minLength(8, 'パスワードは8文字以上である必要があります'),
v.regex(/[A-Z]/, 'パスワードには大文字を含む必要があります'),
v.regex(/[0-9]/, 'パスワードには数字を含む必要があります')
);
const UserRegistrationSchema = v.object({
username: v.pipe(
v.string(),
v.minLength(3, 'ユーザー名は3文字以上である必要があります'),
v.maxLength(20, 'ユーザー名は20文字以下である必要があります')
),
email: EmailSchema,
password: PasswordSchema,
age: v.pipe(
v.number(),
v.minValue(18, '18歳以上である必要があります'),
v.maxValue(120, '有効な年齢を入力してください')
)
});
組み込みスキーマとアクション
スキーマ型
// プリミティブ型
v.string()
v.number()
v.boolean()
v.date()
v.null_()
v.undefined_()
// 複合型
v.array(v.string())
v.object({ name: v.string() })
v.union([v.string(), v.number()])
v.tuple([v.string(), v.number()])
v.record(v.string()) // Record<string, string>
// 特殊型
v.literal('success')
v.enum_(['red', 'green', 'blue'])
v.optional(v.string())
v.nullable(v.string())
バリデーションアクション
// 文字列バリデーション
v.minLength(5)
v.maxLength(100)
v.length(10)
v.email()
v.url()
v.regex(/^[A-Z]+$/)
// 数値バリデーション
v.minValue(0)
v.maxValue(100)
v.integer()
v.finite()
// 配列バリデーション
v.minItems(1)
v.maxItems(10)
v.includes('required-item')
カスタムバリデーション
// カスタムバリデーション関数
const isValidAge = v.custom<number>((input) => {
if (typeof input !== 'number' || input < 0 || input > 150) {
return {
message: '年齢は0から150の間である必要があります',
};
}
return { input };
});
const AgeSchema = v.pipe(v.number(), isValidAge);
// 複雑なカスタムバリデーション
const UniqueUsernameSchema = v.pipe(
v.string(),
v.custom(async (input) => {
const exists = await checkUsernameExists(input);
if (exists) {
return {
message: 'このユーザー名は既に使用されています',
};
}
return { input };
})
);
エラーハンドリング
// エラー詳細の処理
const result = v.safeParse(UserSchema, invalidData);
if (!result.success) {
result.issues.forEach(issue => {
console.log(`フィールド: ${issue.path?.map(p => p.key).join('.')}`);
console.log(`エラー: ${issue.message}`);
console.log(`受信値: ${issue.input}`);
});
}
// カスタムエラーフォーマット
function formatErrors(issues: v.Issues) {
return issues.map(issue => ({
field: issue.path?.map(p => p.key).join('.') || 'root',
message: issue.message,
code: issue.type,
value: issue.input
}));
}
// フォームバリデーション用のエラーハンドラー
function validateForm(data: unknown) {
const result = v.safeParse(FormSchema, data);
if (result.success) {
return { data: result.output, errors: null };
}
const errors = result.issues.reduce((acc, issue) => {
const field = issue.path?.map(p => p.key).join('.') || 'general';
acc[field] = issue.message;
return acc;
}, {} as Record<string, string>);
return { data: null, errors };
}
TypeScript統合
// 型推論
const UserSchema = v.object({
id: v.number(),
name: v.string(),
profile: v.object({
bio: v.string(),
avatar: v.optional(v.string())
})
});
// スキーマから型を推論
type User = v.InferInput<typeof UserSchema>;
type ValidatedUser = v.InferOutput<typeof UserSchema>;
// 変換を含むスキーマ
const TransformSchema = v.pipe(
v.string(),
v.transform(input => input.toUpperCase())
);
type Input = v.InferInput<typeof TransformSchema>; // string
type Output = v.InferOutput<typeof TransformSchema>; // string (transformed)
// 部分型やPick型の作成
const PartialUserSchema = v.partial(UserSchema);
const UserNameSchema = v.pick(UserSchema, ['name']);
const UserWithoutIdSchema = v.omit(UserSchema, ['id']);
実践的な例
フォームバリデーション(React)
import { useState } from 'react';
import * as v from 'valibot';
const ContactFormSchema = v.object({
name: v.pipe(
v.string(),
v.minLength(1, '名前は必須です'),
v.maxLength(50, '名前は50文字以下である必要があります')
),
email: v.pipe(
v.string(),
v.email('有効なメールアドレスを入力してください')
),
message: v.pipe(
v.string(),
v.minLength(10, 'メッセージは10文字以上である必要があります'),
v.maxLength(1000, 'メッセージは1000文字以下である必要があります')
)
});
type ContactForm = v.InferInput<typeof ContactFormSchema>;
export function ContactForm() {
const [formData, setFormData] = useState<ContactForm>({
name: '',
email: '',
message: ''
});
const [errors, setErrors] = useState<Record<string, string>>({});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const result = v.safeParse(ContactFormSchema, formData);
if (result.success) {
// フォーム送信処理
console.log('バリデーション成功:', result.output);
setErrors({});
} else {
// エラー表示
const formErrors = result.issues.reduce((acc, issue) => {
const field = issue.path?.map(p => p.key).join('.') || '';
acc[field] = issue.message;
return acc;
}, {} as Record<string, string>);
setErrors(formErrors);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="text"
placeholder="名前"
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
/>
{errors.name && <span className="error">{errors.name}</span>}
</div>
{/* その他のフィールド */}
<button type="submit">送信</button>
</form>
);
}
APIリクエストバリデーション
import * as v from 'valibot';
// APIリクエストスキーマ
const CreateUserRequestSchema = v.object({
name: v.pipe(v.string(), v.minLength(1)),
email: v.pipe(v.string(), v.email()),
role: v.union([v.literal('admin'), v.literal('user'), v.literal('guest')])
});
const UpdateUserRequestSchema = v.object({
id: v.pipe(v.number(), v.integer()),
name: v.optional(v.pipe(v.string(), v.minLength(1))),
email: v.optional(v.pipe(v.string(), v.email())),
role: v.optional(v.union([v.literal('admin'), v.literal('user'), v.literal('guest')]))
});
// Express.jsでの使用例
app.post('/api/users', (req, res) => {
const result = v.safeParse(CreateUserRequestSchema, req.body);
if (!result.success) {
return res.status(400).json({
error: 'バリデーションエラー',
details: result.issues.map(issue => ({
field: issue.path?.map(p => p.key).join('.'),
message: issue.message
}))
});
}
// バリデーション成功時の処理
const userData = result.output;
// データベースに保存など
});
Tree Shakingの最適化
// 必要な機能のみをインポート
import { object, string, number, parse } from 'valibot';
// 使用しない機能は自動的にバンドルから除外される
const UserSchema = object({
id: number(),
name: string()
});
const result = parse(UserSchema, data);
まとめ
Valibotは、軽量性と型安全性を両立した優れたバリデーションライブラリです。モジュラー設計により、必要な機能のみを使用することでバンドルサイズを大幅に削減できます。TypeScriptとの親和性が高く、開発体験と実行時性能の両方を向上させることができます。
主な利点
- 軽量: 依存関係なしでバンドルサイズを最小化
- 型安全: 完全なTypeScript統合と型推論
- モジュラー: Tree Shakingに最適化された設計
- 高性能: 大規模なスキーマでも高速処理
- 開発体験: 直感的なAPIと豊富なドキュメント
フォームバリデーション、APIリクエスト検証、データ変換など、様々な用途で活用できる汎用性の高いライブラリです。