Библиотека Zod предоставляет мощный механизм проверки данных,
выходящий за пределы простых декларативных схем. Когда требуется
учитывать взаимосвязи между полями, динамические условия или сложные
бизнес-правила, используются методы refine и
superRefine.
Метод refine позволяет добавить пользовательскую
проверку к уже определённой схеме. Он применяется, когда структура
данных корректна на уровне типов, но требуется дополнительная логика
валидации.
schema.refine((value) => boolean, {
message: string
})
Функция получает полностью распарсенное значение и должна вернуть
true или false.
import { z } from "zod";
const schema = z.object({
password: z.string(),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Пароли не совпадают",
path: ["confirmPassword"]
});
Здесь проверка выполняется на уровне объекта целиком, что невозможно выразить через стандартные валидаторы полей.
Несмотря на удобство, refine имеет ряд ограничений:
Эти ограничения становятся критичными при сложной валидации форм и бизнес-логики.
Метод superRefine решает проблемы refine,
предоставляя доступ к контексту ошибок и возможность добавлять несколько
сообщений одновременно.
schema.superRefine((value, ctx) => {
// логика проверки
})
ctx — объект контекста, содержащий метод
addIssue.
const schema = z.object({
password: z.string(),
confirmPassword: z.string(),
}).superRefine((data, ctx) => {
if (data.password.length < 8) {
ctx.addIssue({
code: "custom",
message: "Пароль слишком короткий",
path: ["password"]
});
}
if (data.password !== data.confirmPassword) {
ctx.addIssue({
code: "custom",
message: "Пароли должны совпадать",
path: ["confirmPassword"]
});
}
});
Здесь каждая ошибка привязывается к конкретному полю, что позволяет формировать детализированную структуру ошибок.
Метод принимает объект с полями:
code — тип ошибки (обычно "custom");message — текст ошибки;path — путь к полю, к которому относится ошибка;superRefine позволяет реализовывать зависимые условия между полями.
const schema = z.object({
type: z.enum(["email", "phone"]),
value: z.string()
}).superRefine((data, ctx) => {
if (data.type === "email" && !data.value.includes("@")) {
ctx.addIssue({
code: "custom",
message: "Некорректный email",
path: ["value"]
});
}
if (data.type === "phone" && data.value.length < 10) {
ctx.addIssue({
code: "custom",
message: "Некорректный номер телефона",
path: ["value"]
});
}
});
Логика становится полностью динамической и зависит от состояния всего объекта.
Особенно полезен метод при проверке коллекций, где элементы зависят друг от друга.
const schema = z.array(z.number()).superRefine((arr, ctx) => {
if (arr.length > 0 && arr[0] !== 0) {
ctx.addIssue({
code: "custom",
message: "Первый элемент должен быть 0",
path: [0]
});
}
for (let i = 1; i < arr.length; i++) {
if (arr[i] < arr[i - 1]) {
ctx.addIssue({
code: "custom",
message: "Массив должен быть неубывающим",
path: [i]
});
}
}
});
Каждая ошибка точно указывает индекс элемента, что критично для пользовательских интерфейсов.
Часто refine и superRefine используются
вместе с базовыми схемами:
const schema = z.object({
username: z.string().min(3),
age: z.number().int()
}).superRefine((data, ctx) => {
if (data.age < 18 && data.username === "admin") {
ctx.addIssue({
code: "custom",
message: "Администратор должен быть совершеннолетним",
path: ["age"]
});
}
});
Базовые ограничения отрабатывают первыми, затем применяется дополнительная логика.
В сложных системах валидация часто отражает бизнес-ограничения, а не только типы данных.
const orderSchema = z.object({
items: z.array(z.object({
price: z.number(),
quantity: z.number()
})),
discountCode: z.string().optional()
}).superRefine((order, ctx) => {
const total = order.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
if (order.discountCode && total < 100) {
ctx.addIssue({
code: "custom",
message: "Скидка доступна только при сумме заказа от 100",
path: ["discountCode"]
});
}
});
Логика выходит за пределы структуры данных и описывает правила предметной области.
Использование superRefine влияет на архитектуру
схем:
Поэтому сложные проверки обычно группируются и минимизируются по количеству операций.
Одним из ключевых преимуществ Zod остаётся строгая типизация. Ни
refine, ни superRefine не изменяют тип
результата — они влияют только на валидность данных.
const schema = z.string().refine((val) => val.length > 3);
type Result = z.infer<typeof schema>; // string
Тип остаётся неизменным независимо от добавленной логики.
При обработке форм часто требуется комбинировать несколько уровней валидации:
object и примитивные
схемы;min, max,
regex;refine;superRefine.Так формируется многоуровневая модель проверки данных, где каждая стадия отвечает за свой уровень абстракции.
superRefine позволяет аккумулировать ошибки в единый результат без остановки проверки:
Это делает возможной интеграцию с UI-формами, где требуется отображение всех ошибок одновременно.