Valibot実践ガイド2025 - 最速・最軽量スキーマバリデーション
Valibotは700バイト未満の超軽量でモジュラー設計のTypeScriptスキーマバリデーションライブラリ。Zodより10倍軽量でありながら同等のパフォーマンスを実現。型安全性、バンドルサイズ最適化、エッジ環境対応の実践的な導入から活用まで完全解説。
手動の型ガードはもう古い?Zod(38.6k★)、Valibot(7.7k★)、Effect(9.4k★)など最新ライブラリを使った型安全な実装方法を徹底解説。バンドルサイズとパフォーマンス比較付き。
Typescript の型ガードは強力ですが、手動実装は煩雑でエラーの温床になりがち。2025 年現在、優れたライブラリを活用することで、より安全で保守しやすいコードが書けるようになりました。
手動の型ガード実装には以下の問題があります:
interface User {
id: string;
name: string;
age: number;
email: string;
}
function isUser(data: unknown): data is User {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
typeof data.id === 'string' &&
'name' in data &&
typeof data.name === 'string' &&
'age' in data &&
typeof data.age === 'number' &&
'email' in data &&
typeof data.email === 'string'
);
}
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
name: z.string(),
age: z.number().int().min(0).max(150),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
// バリデーション
const result = UserSchema.safeParse(data);
if (result.success) {
// result.data は User型
console.log(result.data);
}
interface User {
id: string;
name: string;
age: number;
email: string;
}
function isUser(data: unknown): data is User {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
typeof data.id === 'string' &&
'name' in data &&
typeof data.name === 'string' &&
'age' in data &&
typeof data.age === 'number' &&
'email' in data &&
typeof data.email === 'string'
);
}
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
name: z.string(),
age: z.number().int().min(0).max(150),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
// バリデーション
const result = UserSchema.safeParse(data);
if (result.success) {
// result.data は User型
console.log(result.data);
}
// APIレスポンスの型安全な処理
const ApiResponseSchema = z.discriminatedUnion('status', [
z.object({
status: z.literal('success'),
data: z.object({
users: z.array(UserSchema),
total: z.number(),
}),
}),
z.object({
status: z.literal('error'),
error: z.object({
code: z.string(),
message: z.string(),
}),
}),
]);
// 型の変換も可能
const DateSchema = z.string().transform((str) => new Date(str));
// カスタムバリデーション
const PasswordSchema = z.string()
.min(8, 'パスワードは8文字以上必要です')
.regex(/[A-Z]/, '大文字を含む必要があります')
.regex(/[0-9]/, '数字を含む必要があります');
Valibot は、Zod より軽量(約 95%小さい、最小 700 バイト)で最速のバリデーションライブラリです。2025 年現在 v1.1.0 がリリースされ、プロダクション利用が増えています。
import * as v from 'valibot';
// スキーマ定義
const UserSchema = v.object({
id: v.string(),
name: v.string(),
age: v.pipe(
v.number(),
v.integer(),
v.minValue(0),
v.maxValue(150)
),
email: v.pipe(v.string(), v.email()),
});
// 使用方法
try {
const user = v.parse(UserSchema, data);
// userは型安全
} catch (error) {
if (v.isValiError(error)) {
console.log(error.issues);
}
}
// 非同期バリデーション
const AsyncUserSchema = v.objectAsync({
email: v.pipeAsync(
v.string(),
v.email(),
v.customAsync(async (email) => {
const exists = await checkEmailExists(email);
return !exists || 'このメールアドレスは既に使用されています';
})
),
});
ライブラリ | GitHubスター | バンドルサイズ | パフォーマンス | 特徴 |
---|---|---|---|---|
Zod | 38.6k | 12.8kb | 標準 | 豊富な機能、大規模エコシステム |
Valibot | 7.7k | 0.7kb~ | 最速 | モジュラー設計、Tree-shaking完全対応 |
Effect | 9.4k | 25kb~ | 高速 | 包括的な型システム、FP指向 |
Yup | 22.8k | 15.2kb | やや遅い | 歴史が長い、Formikとの相性良 |
io-ts | 6.7k | 8.5kb | 高速 | 関数型プログラミング指向 |
Effect は、より高度な型安全性とエラーハンドリングを提供します。
import { Effect, Schema } from '@effect/schema';
// スキーマ定義
const User = Schema.struct({
id: Schema.string,
name: Schema.string,
age: Schema.number.pipe(
Schema.int(),
Schema.between(0, 150)
),
email: Schema.string.pipe(Schema.pattern(/^.+@.+$/)),
});
// Effectを使った処理
const fetchUser = (id: string) =>
Effect.tryPromise({
try: () => fetch(`/api/users/${id}`).then(r => r.json()),
catch: () => new Error('Network error'),
}).pipe(
Effect.flatMap((data) => Schema.parse(User)(data)),
Effect.catchTag('ParseError', (error) =>
Effect.fail(new Error('Invalid user data'))
)
);
// 実行
Effect.runPromise(fetchUser('123'))
.then(user => console.log(user))
.catch(error => console.error(error));
ts-pattern は、型ガードを超えたパターンマッチングを提供します。
import { match, P } from 'ts-pattern';
// Union型の処理
type Shape =
| { type: 'circle'; radius: number }
| { type: 'rectangle'; width: number; height: number }
| { type: 'triangle'; base: number; height: number };
const area = (shape: Shape) =>
match(shape)
.with({ type: 'circle' }, ({ radius }) => Math.PI * radius ** 2)
.with({ type: 'rectangle' }, ({ width, height }) => width * height)
.with({ type: 'triangle' }, ({ base, height }) => (base * height) / 2)
.exhaustive();
// APIレスポンスの処理
const handleResponse = (response: unknown) =>
match(response)
.with({ status: P.number.between(200, 299) }, () => 'Success')
.with({ status: 400 }, () => 'Bad Request')
.with({ status: 401 }, () => 'Unauthorized')
.with({ status: P.number.between(500, 599) }, () => 'Server Error')
.otherwise(() => 'Unknown Error');
// 複雑な条件分岐
const processUser = (user: unknown) =>
match(user)
.with(
{
age: P.number.gte(18),
email: P.string.includes('@'),
role: P.union('admin', 'user'),
},
(validUser) => {
// validUserは型が絞り込まれている
return `Valid ${validUser.role}`;
}
)
.with({ age: P.number.lt(18) }, () => 'Too young')
.otherwise(() => 'Invalid user');
// React Hook FormとZodの組み合わせ
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const FormSchema = z.object({
name: z.string().min(1, '名前は必須です'),
email: z.string().email('有効なメールアドレスを入力してください'),
age: z.number().min(18, '18歳以上である必要があります'),
agree: z.boolean().refine((val) => val === true, {
message: '利用規約に同意してください',
}),
});
type FormData = z.infer<typeof FormSchema>;
function MyForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(FormSchema),
});
const onSubmit = (data: FormData) => {
// dataは型安全
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} />
{errors.name && <span>{errors.name.message}</span>}
{/* 他のフィールド */}
</form>
);
}
// tRPCとZodを使った型安全なAPI
import { z } from 'zod';
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
const userRouter = t.router({
getById: t.procedure
.input(z.object({ id: z.string().uuid() }))
.query(async ({ input }) => {
const user = await db.user.findById(input.id);
return UserSchema.parse(user);
}),
create: t.procedure
.input(
z.object({
name: z.string(),
email: z.string().email(),
password: z.string().min(8),
})
)
.mutation(async ({ input }) => {
const hashedPassword = await hash(input.password);
const user = await db.user.create({
...input,
password: hashedPassword,
});
return UserSchema.parse(user);
}),
});
// クライアント側は自動的に型安全
const user = await trpc.user.getById.query({ id: '123' });
// ZustandとValibotの組み合わせ
import { create } from 'zustand';
import * as v from 'valibot';
const UserStateSchema = v.object({
user: v.nullable(
v.object({
id: v.string(),
name: v.string(),
email: v.string(),
})
),
isLoading: v.boolean(),
error: v.nullable(v.string()),
});
type UserState = v.InferOutput<typeof UserStateSchema>;
interface UserActions {
setUser: (user: UserState['user']) => void;
setLoading: (isLoading: boolean) => void;
setError: (error: string | null) => void;
}
const useUserStore = create<UserState & UserActions>((set) => ({
user: null,
isLoading: false,
error: null,
setUser: (user) => {
// バリデーション付きで更新
const validUser = v.parse(
UserStateSchema.entries.user,
user
);
set({ user: validUser });
},
setLoading: (isLoading) => set({ isLoading }),
setError: (error) => set({ error }),
}));
*相対的なパフォーマンススコア(バリデーション速度・2025 年ベンチマーク)
バンドルサイズが重要な場合
エコシステムとドキュメントが充実
複雑なエラーハンドリングが必要な場合
複雑な条件分岐を扱う場合
スキーマファーストアプローチ
// スキーマから型を生成
const UserSchema = z.object({...});
type User = z.infer<typeof UserSchema>;
エラーメッセージの日本語化
import { z } from 'zod';
import { zodI18nMap } from 'zod-i18n-map';
import translation from 'zod-i18n-map/locales/ja/zod.json';
z.setErrorMap(zodI18nMap);
段階的な導入
Typescript 7との連携(2025年新機能)
// tsgoコンパイラでの最適化
// 型ガードのパフォーマンスが自動的に向上
型ガードライブラリを活用することで、typescript プロジェクトの型安全性と開発効率が大幅に向上します。プロジェクトの規模と要件に応じて、適切なライブラリを選択しましょう。
型安全性は、バグの早期発見と開発体験の向上につながります。今日から導入して、より堅牢な typescript コードを書きましょう!
この記事で紹介したライブラリやテクニックについて、より詳しく学べるリソースをカテゴリ別にまとめました。