Yup
JavaScriptとTypeScript向けのスキーマビルダーによるオブジェクトバリデーションライブラリ
概要
Yupは、JavaScriptおよびTypeScript向けの強力なスキーマビルダーとバリデーションライブラリです。直感的なAPIを使用して、複雑なデータ構造の検証ルールを宣言的に定義できます。Joiライブラリに影響を受けながらも、クライアントサイドでの使用に最適化されており、ブラウザ環境でも軽量に動作します。
主な特徴
- 宣言的なスキーマ定義: チェーンメソッドによる直感的なバリデーションルールの構築
- TypeScript完全対応: 優れた型推論とInferTypeによる型生成
- 豊富な組み込みバリデーター: 文字列、数値、日付、配列、オブジェクトなどの検証
- カスタムバリデーション: 独自の検証ロジックの実装が容易
- 条件付きバリデーション: 動的な検証ルールの適用
- 国際化対応: カスタムエラーメッセージとローカライゼーション
- 非同期バリデーション: APIコールを含む非同期検証のサポート
インストール
# npm
npm install yup
# yarn
yarn add yup
# pnpm
pnpm add yup
基本的な使い方
スキーマの定義と検証
import * as yup from 'yup';
// スキーマの定義
const userSchema = yup.object({
name: yup.string().required('名前は必須です'),
age: yup.number().positive().integer().required('年齢は必須です'),
email: yup.string().email('有効なメールアドレスを入力してください'),
website: yup.string().url().nullable(),
createdOn: yup.date().default(() => new Date()),
});
// 検証の実行
const user = {
name: '田中太郎',
age: 30,
email: '[email protected]',
};
// 同期的な検証
try {
const validatedUser = userSchema.validateSync(user);
console.log(validatedUser);
} catch (err) {
console.error(err.errors);
}
// 非同期検証
userSchema
.validate(user)
.then(validatedUser => {
console.log(validatedUser);
})
.catch(err => {
console.error(err.errors);
});
組み込みバリデーションメソッド
文字列バリデーション
const stringSchema = yup.string()
.required('必須項目です')
.min(3, '3文字以上入力してください')
.max(20, '20文字以内で入力してください')
.matches(/^[a-zA-Z0-9]+$/, '英数字のみ使用可能です')
.email('有効なメールアドレスを入力してください')
.url('有効なURLを入力してください')
.lowercase('小文字で入力してください')
.uppercase('大文字で入力してください')
.trim(); // 前後の空白を削除
数値バリデーション
const numberSchema = yup.number()
.required('必須項目です')
.positive('正の数を入力してください')
.negative('負の数を入力してください')
.integer('整数を入力してください')
.min(0, '0以上の値を入力してください')
.max(100, '100以下の値を入力してください')
.lessThan(100, '100未満の値を入力してください')
.moreThan(0, '0より大きい値を入力してください');
日付バリデーション
const dateSchema = yup.date()
.required('必須項目です')
.min(new Date('2020-01-01'), '2020年1月1日以降の日付を選択してください')
.max(new Date(), '今日以前の日付を選択してください');
配列バリデーション
const arraySchema = yup.array()
.of(yup.string().required())
.required('必須項目です')
.min(1, '少なくとも1つ選択してください')
.max(5, '最大5つまで選択可能です');
// 一意性の検証
const uniqueArraySchema = yup.array()
.of(yup.string())
.test('unique', '重複する値があります', (values) => {
return values?.length === new Set(values).size;
});
オブジェクトバリデーション
const addressSchema = yup.object({
street: yup.string().required('住所は必須です'),
city: yup.string().required('市区町村は必須です'),
state: yup.string().required('都道府県は必須です'),
zip: yup.string().matches(/^\d{3}-?\d{4}$/, '有効な郵便番号を入力してください'),
});
const personSchema = yup.object({
name: yup.string().required(),
age: yup.number().positive().integer(),
address: addressSchema,
hobbies: yup.array().of(yup.string()),
});
カスタムバリデーション
テストメソッドの使用
const passwordSchema = yup.string()
.required('パスワードは必須です')
.min(8, 'パスワードは8文字以上必要です')
.test('has-uppercase', '大文字を含めてください', (value) => {
return /[A-Z]/.test(value || '');
})
.test('has-lowercase', '小文字を含めてください', (value) => {
return /[a-z]/.test(value || '');
})
.test('has-number', '数字を含めてください', (value) => {
return /\d/.test(value || '');
});
カスタムメソッドの追加
// Yupを拡張してカスタムメソッドを追加
yup.addMethod(yup.string, 'phone', function(message) {
return this.test('phone', message || '有効な電話番号を入力してください', function(value) {
const phoneRegex = /^0\d{1,4}-?\d{1,4}-?\d{4}$/;
return !value || phoneRegex.test(value);
});
});
// TypeScript向けの型定義
declare module 'yup' {
interface StringSchema {
phone(message?: string): StringSchema;
}
}
// 使用例
const phoneSchema = yup.string().phone().required();
条件付きバリデーション
when()メソッドの使用
const purchaseSchema = yup.object({
isMember: yup.boolean(),
memberNumber: yup.string().when('isMember', {
is: true,
then: (schema) => schema.required('会員番号は必須です'),
otherwise: (schema) => schema.nullable(),
}),
paymentMethod: yup.string().required(),
creditCardNumber: yup.string().when('paymentMethod', {
is: 'credit',
then: (schema) => schema
.required('クレジットカード番号は必須です')
.matches(/^\d{16}$/, '16桁の数字を入力してください'),
otherwise: (schema) => schema.nullable(),
}),
});
複数フィールドの条件
const formSchema = yup.object({
startDate: yup.date().required('開始日は必須です'),
endDate: yup.date()
.required('終了日は必須です')
.when('startDate', (startDate, schema) => {
return schema.min(startDate, '終了日は開始日より後の日付を選択してください');
}),
});
エラーハンドリング
カスタムエラーメッセージ
const schema = yup.object({
email: yup.string()
.required('メールアドレスを入力してください')
.email('正しいメールアドレスの形式で入力してください'),
password: yup.string()
.required('パスワードを入力してください')
.min(8, ({ min }) => `パスワードは${min}文字以上必要です`),
});
// バリデーションエラーの取得
try {
schema.validateSync(data, { abortEarly: false });
} catch (err) {
if (err instanceof yup.ValidationError) {
// すべてのエラーを取得
console.log(err.errors);
// フィールドごとのエラー
err.inner.forEach((error) => {
console.log(`${error.path}: ${error.message}`);
});
}
}
非同期エラーハンドリング
const asyncSchema = yup.object({
email: yup.string()
.email()
.test('email-exists', 'このメールアドレスは既に登録されています', async (value) => {
// API呼び出しなどの非同期処理
const exists = await checkEmailExists(value);
return !exists;
}),
});
asyncSchema.validate(data)
.catch((err: yup.ValidationError) => {
console.error('Validation failed:', err.errors);
});
TypeScript統合
型推論とInferType
import * as yup from 'yup';
const userSchema = yup.object({
id: yup.number().required(),
name: yup.string().required(),
email: yup.string().email().required(),
age: yup.number().positive().nullable(),
roles: yup.array().of(yup.string().required()).required(),
settings: yup.object({
theme: yup.string().oneOf(['light', 'dark']).required(),
notifications: yup.boolean().required(),
}).required(),
});
// スキーマから型を推論
type User = yup.InferType<typeof userSchema>;
// {
// id: number;
// name: string;
// email: string;
// age: number | null;
// roles: string[];
// settings: {
// theme: 'light' | 'dark';
// notifications: boolean;
// };
// }
// 型安全な検証
const validateUser = async (data: unknown): Promise<User> => {
return await userSchema.validate(data);
};
ジェネリック型の使用
interface UserInput {
name: string;
email: string;
age?: number;
}
const createUserSchema = <T extends UserInput>(): yup.Schema<T> => {
return yup.object({
name: yup.string().required(),
email: yup.string().email().required(),
age: yup.number().positive().optional(),
}) as yup.Schema<T>;
};
フォームライブラリとの統合
React Hook Formとの統合
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
const schema = yup.object({
firstName: yup.string().required('名は必須です'),
lastName: yup.string().required('姓は必須です'),
email: yup.string().email('有効なメールアドレスを入力してください').required('メールアドレスは必須です'),
});
type FormData = yup.InferType<typeof schema>;
function MyForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: yupResolver(schema),
});
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('firstName')} />
{errors.firstName && <span>{errors.firstName.message}</span>}
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<button type="submit">送信</button>
</form>
);
}
Formikとの統合
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as yup from 'yup';
const validationSchema = yup.object({
email: yup.string().email('無効なメールアドレス').required('必須'),
password: yup.string().min(8, '8文字以上').required('必須'),
});
function LoginForm() {
return (
<Formik
initialValues={{ email: '', password: '' }}
validationSchema={validationSchema}
onSubmit={(values) => {
console.log(values);
}}
>
{({ errors, touched }) => (
<Form>
<Field name="email" type="email" />
<ErrorMessage name="email" component="div" />
<Field name="password" type="password" />
<ErrorMessage name="password" component="div" />
<button type="submit">ログイン</button>
</Form>
)}
</Formik>
);
}
実践的な例
ユーザー登録フォームの検証
const registrationSchema = yup.object({
username: yup.string()
.required('ユーザー名は必須です')
.min(3, 'ユーザー名は3文字以上必要です')
.max(20, 'ユーザー名は20文字以内にしてください')
.matches(/^[a-zA-Z0-9_]+$/, 'ユーザー名は英数字とアンダースコアのみ使用可能です'),
email: yup.string()
.required('メールアドレスは必須です')
.email('有効なメールアドレスを入力してください')
.test('email-domain', '企業メールアドレスを使用してください', (value) => {
return value ? !value.endsWith('@gmail.com') && !value.endsWith('@yahoo.com') : false;
}),
password: yup.string()
.required('パスワードは必須です')
.min(8, 'パスワードは8文字以上必要です')
.matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,
'パスワードは大文字、小文字、数字、特殊文字を含む必要があります'
),
confirmPassword: yup.string()
.required('パスワード確認は必須です')
.oneOf([yup.ref('password')], 'パスワードが一致しません'),
birthDate: yup.date()
.required('生年月日は必須です')
.max(new Date(), '未来の日付は選択できません')
.test('age', '18歳以上である必要があります', (value) => {
if (!value) return false;
const age = new Date().getFullYear() - value.getFullYear();
return age >= 18;
}),
terms: yup.boolean()
.required('利用規約への同意は必須です')
.oneOf([true], '利用規約に同意する必要があります'),
});
動的フォームの検証
const dynamicFormSchema = yup.object({
fields: yup.array().of(
yup.object({
type: yup.string().oneOf(['text', 'number', 'email', 'date']).required(),
label: yup.string().required('ラベルは必須です'),
value: yup.mixed().when('type', {
is: 'text',
then: () => yup.string().required('テキストフィールドは必須です'),
otherwise: yup.mixed().when('type', {
is: 'number',
then: () => yup.number().required('数値フィールドは必須です'),
otherwise: yup.mixed().when('type', {
is: 'email',
then: () => yup.string().email('有効なメールアドレスを入力してください').required(),
otherwise: yup.mixed().when('type', {
is: 'date',
then: () => yup.date().required('日付フィールドは必須です'),
}),
}),
}),
}),
})
),
});
ベストプラクティス
1. スキーマの再利用
// 共通スキーマの定義
const commonSchemas = {
email: yup.string().email('有効なメールアドレスを入力してください').required('メールアドレスは必須です'),
password: yup.string().min(8, 'パスワードは8文字以上必要です').required('パスワードは必須です'),
phoneNumber: yup.string().matches(/^0\d{1,4}-?\d{1,4}-?\d{4}$/, '有効な電話番号を入力してください'),
};
// スキーマの組み合わせ
const loginSchema = yup.object({
email: commonSchemas.email,
password: commonSchemas.password,
});
const profileSchema = yup.object({
email: commonSchemas.email,
phone: commonSchemas.phoneNumber,
bio: yup.string().max(500, '自己紹介は500文字以内で入力してください'),
});
2. エラーメッセージの国際化
import { setLocale } from 'yup';
// 日本語ロケールの設定
setLocale({
mixed: {
default: '無効な値です',
required: '必須項目です',
oneOf: '次のいずれかの値にする必要があります: ${values}',
notOneOf: '次の値以外にする必要があります: ${values}',
},
string: {
length: '${length}文字にする必要があります',
min: '${min}文字以上入力してください',
max: '${max}文字以内で入力してください',
email: '有効なメールアドレスを入力してください',
url: '有効なURLを入力してください',
trim: '前後の空白は使用できません',
lowercase: '小文字で入力してください',
uppercase: '大文字で入力してください',
},
number: {
min: '${min}以上の値を入力してください',
max: '${max}以下の値を入力してください',
lessThan: '${less}未満の値を入力してください',
moreThan: '${more}より大きい値を入力してください',
positive: '正の数を入力してください',
negative: '負の数を入力してください',
integer: '整数を入力してください',
},
date: {
min: '${min}以降の日付を選択してください',
max: '${max}以前の日付を選択してください',
},
array: {
min: '${min}個以上選択してください',
max: '${max}個以内で選択してください',
},
});
3. パフォーマンスの最適化
// lazyを使用した遅延評価
const userSchema = yup.lazy((value) => {
if (value?.type === 'admin') {
return yup.object({
type: yup.string().required(),
adminCode: yup.string().required('管理者コードは必須です'),
permissions: yup.array().of(yup.string()).required(),
});
}
return yup.object({
type: yup.string().required(),
email: yup.string().email().required(),
});
});
// 部分的な検証
const partialSchema = userSchema.pick(['email', 'name']);
// 条件付きの必須フィールド
const conditionalSchema = yup.object({
hasAddress: yup.boolean(),
address: yup.string().when('hasAddress', {
is: true,
then: (schema) => schema.required('住所は必須です'),
}),
});
他のバリデーションライブラリとの比較
Zodとの比較
- Yup: より成熟したエコシステム、豊富なバリデーションメソッド
- Zod: TypeScript-firstの設計、より優れた型推論
Joiとの比較
- Yup: クライアントサイド向けに最適化、軽量
- Joi: サーバーサイド向け、より豊富な機能
ValibotやSuperstructとの比較
- Yup: 広範な採用実績、フォームライブラリとの統合が充実
- Valibot/Superstruct: より軽量、モジュラーな設計
まとめ
Yupは、JavaScriptとTypeScriptプロジェクトにおいて、強力で柔軟なバリデーション機能を提供します。宣言的なAPIにより、複雑なバリデーションルールも読みやすく保守しやすいコードで実装できます。React Hook FormやFormikなどの人気フォームライブラリとの優れた統合により、フォームバリデーションの実装が大幅に簡素化されます。プロジェクトの要件に応じて、適切なバリデーション戦略を選択し、Yupの強力な機能を活用してください。