Стандартных методов библиотеки Yup часто достаточно для
проверки типовых сценариев:
Однако в реальных приложениях возникают требования, которые невозможно удобно реализовать встроенными средствами:
Для решения подобных задач используются кастомные методы валидации.
test()Базовый механизм создания пользовательской проверки — метод
test().
Общий синтаксис:
yup.string().test(
name,
message,
testFunction
)
Параметры:
| Параметр | Описание |
|---|---|
name |
Уникальное имя проверки |
message |
Текст ошибки |
testFunction |
Функция проверки |
Проверка строки на наличие слова admin.
import * as yup from 'yup';
const schema = yup.string().test(
'contains-admin',
'Строка должна содержать слово admin',
value => {
return value.includes('admin');
}
);
Проверка:
schema.validate('super-admin');
Результат:
'super-admin'
Ошибка:
schema.validate('manager');
Результат:
ValidationError: Строка должна содержать слово admin
Функция проверки должна возвращать:
true — проверка пройдена;false — ошибка валидации.const schema = yup.string().test(
'only-uppercase',
'Допустимы только заглавные буквы',
value => /^[A-Z]+$/.test(value)
);
undefined и
nullКастомная проверка вызывается даже при отсутствии значения.
Неправильный вариант:
const schema = yup.string().test(
'check-length',
'Минимум 5 символов',
value => value.length >= 5
);
Ошибка:
Cannot read properties of undefined
Правильный вариант:
const schema = yup.string().test(
'check-length',
'Минимум 5 символов',
value => {
if (!value) return true;
return value.length >= 5;
}
);
thisВнутри функции проверки доступен контекст через
this.
const schema = yup.string().test(
'debug',
'Ошибка',
function(value) {
console.log(this);
return true;
}
);
Контекст содержит:
| Свойство | Назначение |
|---|---|
path |
Имя поля |
parent |
Родительский объект |
options |
Опции валидации |
createError() |
Создание кастомной ошибки |
schema |
Текущая схема |
Одна из самых важных возможностей — доступ к родительскому объекту.
const schema = yup.object({
password: yup.string().required(),
confirmPassword: yup.string().test(
'password-match',
'Пароли не совпадают',
function(value) {
return value === this.parent.password;
}
)
});
createErrorМетод createError() позволяет динамически формировать
сообщение.
const schema = yup.string().test(
'min-words',
'Ошибка',
function(value) {
const words = value.trim().split(' ');
if (words.length < 3) {
return this.createError({
message: `Минимум слов: 3. Сейчас: ${words.length}`
});
}
return true;
}
);
Практический пример.
const passwordSchema = yup.string().test(
'strong-password',
'Слабый пароль',
function(value) {
if (!value) return true;
const hasUpperCase = /[A-Z]/.test(value);
const hasLowerCase = /[a-z]/.test(value);
const hasDigit = /\d/.test(value);
const hasSpecial = /[@$!%*?&]/.test(value);
if (!hasUpperCase) {
return this.createError({
message: 'Пароль должен содержать заглавную букву'
});
}
if (!hasLowerCase) {
return this.createError({
message: 'Пароль должен содержать строчную букву'
});
}
if (!hasDigit) {
return this.createError({
message: 'Пароль должен содержать цифру'
});
}
if (!hasSpecial) {
return this.createError({
message: 'Пароль должен содержать спецсимвол'
});
}
return true;
}
);
Yup поддерживает асинхронную валидацию.
Например, проверка уникальности email.
const schema = yup.string().test(
'email-exists',
'Email уже используется',
async function(value) {
const response = await fetch('/api/check-email', {
method: 'POST',
body: JSON.stringify({ email: value })
});
const data = await response.json();
return !data.exists;
}
);
Асинхронная функция может возвращать createError().
const schema = yup.string().test(
'blocked-email',
'Ошибка',
async function(value) {
const blocked = ['admin@mail.com'];
if (blocked.includes(value)) {
return this.createError({
message: 'Email заблокирован'
});
}
return true;
}
);
Метод test() позволяет передавать параметры через
объект.
const schema = yup.string().test({
name: 'min-length',
message: 'Минимальная длина — 10 символов',
test(value) {
if (!value) return true;
return value.length >= 10;
}
});
Такой формат более удобен для сложных проверок.
Кастомную проверку можно вынести в отдельную функцию.
const isStrongPassword = value => {
if (!value) return false;
return (
/[A-Z]/.test(value) &&
/[a-z]/.test(value) &&
/\d/.test(value)
);
};
const schema = yup.string().test(
'strong-password',
'Слабый пароль',
isStrongPassword
);
addMethodYup позволяет расширять встроенные типы.
Синтаксис:
yup.addMethod(type, name, method)
Пример метода isPalindrome.
import * as yup from 'yup';
yup.addMethod(yup.string, 'isPalindrome', function(message) {
return this.test(
'is-palindrome',
message,
function(value) {
if (!value) return true;
const reversed = value
.split('')
.reverse()
.join('');
return value === reversed;
}
);
});
Использование:
const schema = yup.string().isPalindrome(
'Строка не является палиндромом'
);
Методы могут принимать аргументы.
yup.addMethod(yup.string, 'minWords', function(min, message) {
return this.test(
'min-words',
message,
function(value) {
if (!value) return true;
const words = value
.trim()
.split(/\s+/);
return words.length >= min;
}
);
});
Пример:
const schema = yup.string().minWords(
5,
'Минимум 5 слов'
);
yup.addMethod(yup.number, 'positiveInteger', function(message) {
return this.test(
'positive-integer',
message,
value => {
if (value === undefined) return true;
return Number.isInteger(value) && value > 0;
}
);
});
Использование:
const schema = yup.number().positiveInteger(
'Только положительные целые числа'
);
yup.addMethod(yup.array, 'unique', function(message) {
return this.test(
'unique',
message,
function(value) {
if (!value) return true;
return new Set(value).size === value.length;
}
);
});
Использование:
const schema = yup.array().unique(
'Массив содержит дубликаты'
);
Без расширения типов TypeScript не знает о новых методах.
Необходимо расширить интерфейс.
declare module 'yup' {
interface StringSchema {
isPalindrome(message: string): StringSchema;
}
}
После этого IDE начнёт распознавать метод.
import * as yup from 'yup';
declare module 'yup' {
interface StringSchema {
strongPassword(message: string): StringSchema;
}
}
yup.addMethod(
yup.string,
'strongPassword',
function(message: string) {
return this.test(
'strong-password',
message,
function(value?: string) {
if (!value) return true;
const valid =
/[A-Z]/.test(value) &&
/[a-z]/.test(value) &&
/\d/.test(value);
return valid;
}
);
}
);
const schema = yup.object({
password: yup
.string()
.required()
.strongPassword('Слабый пароль')
});
exclusiveПо умолчанию проверки с одинаковым именем могут накапливаться.
yup.string()
.test('rule', 'Ошибка 1', () => true)
.test('rule', 'Ошибка 2', () => true);
Для замены предыдущего теста используется exclusive.
yup.string().test({
name: 'rule',
exclusive: true,
message: 'Ошибка',
test: () => true
});
В validate() можно передавать внешний контекст.
const schema = yup.string().test(
'role-check',
'Недостаточно прав',
function(value) {
return this.options.context.role === 'admin';
}
);
Проверка:
schema.validate('test', {
context: {
role: 'admin'
}
});
Пример динамической проверки.
const schema = yup.object({
type: yup.string().required(),
value: yup.string().test(
'dynamic-validation',
'Некорректное значение',
function(value) {
const { type } = this.parent;
if (type === 'email') {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
if (type === 'phone') {
return /^\+7\d{10}$/.test(value);
}
return true;
}
)
});
В крупных проектах кастомные методы обычно выносятся в отдельную директорию.
Структура:
validators/
├── stringValidators.js
├── numberValidators.js
├── arrayValidators.js
└── index.js
Пример:
// validators/stringValidators.js
import * as yup from 'yup';
export const registerStringValidators = () => {
yup.addMethod(
yup.string,
'slug',
function(message) {
return this.test(
'slug',
message,
value => {
if (!value) return true;
return /^[a-z0-9-]+$/.test(value);
}
);
}
);
};
Регистрация:
import { registerStringValidators } from './validators/stringValidators';
registerStringValidators();
thisНеправильно:
test: (value) => {
return this.parent;
}
У стрелочных функций отсутствует собственный this.
Правильно:
test: function(value) {
return this.parent;
}
undefinedНеправильно:
value.length > 5
Правильно:
if (!value) return true;
Плохая практика:
test(value) {
// 300 строк логики
}
Лучше:
test(value) {
return validateBusinessRule(value);
}
Методы можно объединять.
const schema = yup.string()
.required()
.min(8)
.strongPassword('Слабый пароль')
.minWords(2, 'Минимум 2 слова');
const passwordSchema = yup
.string()
.required()
.strongPassword('Слабый пароль');
const userSchema = yup.object({
password: passwordSchema
});
const adminSchema = yup.object({
password: passwordSchema
});
YupResolver используется через
@hookform/resolvers/yup.
import { yupResolver } from '@hookform/resolvers/yup';
const form = useForm({
resolver: yupResolver(schema)
});
Кастомные методы работают автоматически.
const schema = yup.object({
password: yup
.string()
.strongPassword('Слабый пароль')
});
Ошибки попадают в formState.errors.
errors.password.message
yup.addMethod(yup.array, 'uniqueEmails', function(message) {
return this.test(
'unique-emails',
message,
function(value) {
if (!value) return true;
const emails = value.map(item => item.email);
return new Set(emails).size === emails.length;
}
);
});
Использование:
const schema = yup.object({
users: yup.array().uniqueEmails(
'Email должны быть уникальными'
)
});
yup.addMethod(yup.date, 'notWeekend', function(message) {
return this.test(
'not-weekend',
message,
function(value) {
if (!value) return true;
const day = value.getDay();
return day !== 0 && day !== 6;
}
);
});
Вместо одного огромного метода лучше использовать несколько небольших.
Плохо:
strongValidation()
Лучше:
yup.string()
.hasUppercase()
.hasLowercase()
.hasNumber()
.hasSpecialCharacter();
Преимущества:
Пример проверки через Jest.
describe('strongPassword', () => {
test('валидный пароль', async () => {
const schema = yup
.string()
.strongPassword('Ошибка');
await expect(
schema.isValid('Admin123')
).resolves.toBe(true);
});
test('невалидный пароль', async () => {
const schema = yup
.string()
.strongPassword('Ошибка');
await expect(
schema.isValid('123')
).resolves.toBe(false);
});
});
Тяжёлые проверки могут замедлять формы.
Особенно опасны:
Оптимизации:
Плохо:
test(function(value) {
// бизнес-логика
})
Лучше:
test(value => validateTaxNumber(value))
Плохо:
test('x1', ...)
Хорошо:
test('valid-tax-number', ...)
Плохо:
'Ошибка'
'Неверно'
'Введите правильно'
Хорошо:
'Неверный формат email'
'Неверный ИИН'
'Неверный номер телефона'
import * as yup from 'yup';
yup.addMethod(yup.string, 'username', function(message) {
return this.test(
'username',
message,
function(value) {
if (!value) return true;
return /^[a-zA-Z0-9_]+$/.test(value);
}
);
});
yup.addMethod(yup.string, 'strongPassword', function(message) {
return this.test(
'strong-password',
message,
function(value) {
if (!value) return true;
return (
/[A-Z]/.test(value) &&
/[a-z]/.test(value) &&
/\d/.test(value)
);
}
);
});
const registrationSchema = yup.object({
username: yup
.string()
.required('Введите логин')
.min(4, 'Минимум 4 символа')
.username('Допустимы только буквы, цифры и _'),
password: yup
.string()
.required('Введите пароль')
.min(8, 'Минимум 8 символов')
.strongPassword('Пароль слишком простой'),
confirmPassword: yup
.string()
.required('Подтвердите пароль')
.test(
'password-match',
'Пароли не совпадают',
function(value) {
return value === this.parent.password;
}
)
});