yupResolver — адаптер между библиотекой валидации
Yup и системой управления формами
React Hook Form. Основная задача — преобразование схемы
Yup в механизм проверки данных формы с автоматическим
выводом типов и поддержкой TypeScript.
Пакет находится в библиотеке @hookform/resolvers.
npm install react-hook-form yup @hookform/resolvers
Базовое подключение:
import { useForm } from "react-hook-form"
import { yupResolver } from "@hookform/resolvers/yup"
import * as yup from "yup"
InferTypeГлавная проблема при работе с формами — дублирование типов:
Yup позволяет генерировать типы автоматически.
const schema = yup.object({
email: yup.string().email().required(),
age: yup.number().required().min(18),
})
type FormData = yup.InferType<typeof schema>
Теперь FormData автоматически синхронизирован со
схемой.
Полученный тип:
type FormData = {
email: string
age: number
}
useFormconst {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: yupResolver(schema),
})
Тип формы теперь связан:
register;errors;handleSubmit.const schema = yup.object({
middleName: yup.string().nullable(),
})
type FormData = yup.InferType<typeof schema>
Результат:
type FormData = {
middleName: string | null | undefined
}
required()requiredconst schema = yup.object({
username: yup.string(),
})
Тип:
{
username?: string | undefined
}
requiredconst schema = yup.object({
username: yup.string().required(),
})
Тип:
{
username: string
}
defined()Метод defined() исключает undefined.
const schema = yup.object({
token: yup.string().defined(),
})
Тип:
{
token: string
}
Разница между required() и defined():
| Метод | Проверяет пустую строку | Убирает undefined |
|---|---|---|
| required() | Да | Да |
| defined() | Нет | Да |
const schema = yup.object({
profile: yup.object({
firstName: yup.string().required(),
lastName: yup.string().required(),
}),
})
type FormData = {
profile: {
firstName: string
lastName: string
}
}
const schema = yup.object({
tags: yup.array(yup.string().required()).required(),
})
Тип:
{
tags: string[]
}
const schema = yup.object({
users: yup.array(
yup.object({
id: yup.number().required(),
name: yup.string().required(),
})
),
})
Тип:
{
users?: {
id: number
name: string
}[]
}
as constПри работе с sel ect-значениями полезно ограничивать строковые литералы.
const schema = yup.object({
role: yup.string().required(),
})
Тип:
role: string
const roles = ["admin", "user", "moderator"] as const
const schema = yup.object({
role: yup.mixed<(typeof roles)[number]>()
.oneOf(roles)
.required(),
})
Тип:
role: "admin" | "user" | "moderator"
enum Status {
Active = "active",
Blocked = "blocked",
}
const schema = yup.object({
status: yup.mixed<Status>()
.oneOf(Object.values(Status))
.required(),
})
Тип:
{
status: Status
}
SchemaOfSchemaOf<T> позволяет описывать схему на основе
готового интерфейса.
interface UserForm {
email: string
age: number
}
const schema: yup.SchemaOf<UserForm> = yup.object({
email: yup.string().email().required(),
age: yup.number().required(),
})
InferType и SchemaOf| Подход | Источник истины |
|---|---|
| InferType | Yup-схема |
| SchemaOf | TypeScript-интерфейс |
На практике чаще используется:
type FormData = yup.InferType<typeof schema>
Причины:
defaultValuesuseForm<FormData>({
defaultValues: {
age: "",
},
})
Ошибка:
Type 'string' is not assignable to type 'number'
useForm<FormData>({
defaultValues: {
age: 18,
},
})
Даже если поле имеет тип number, браузер возвращает
строку.
<input type="number" {...register("age")} />
Фактически значение:
"25"
valueAsNumber<input
type="number"
{...register("age", {
valueAsNumber: true,
})}
/>
Теперь:
25
const schema = yup.object({
isAdmin: yup.boolean().required(),
})
{
isAdmin: boolean
}
const schema = yup.object({
createdAt: yup.date().required(),
})
Тип:
{
createdAt: Date
}
HTML input возвращает строку.
<input type="date" />
Результат:
"2026-01-01"
const schema = yup.object({
createdAt: yup.date().transform((value, originalValue) => {
return originalValue ? new Date(originalValue) : value
}),
})
whenconst schema = yup.object({
hasPhone: yup.boolean().required(),
phone: yup.string().when("hasPhone", {
is: true,
then: schema => schema.required(),
otherwise: schema => schema.optional(),
}),
})
Тип:
{
hasPhone: boolean
phone?: string
}
whenTypeScript не умеет полноценно выводить условные типы из
when.
Поэтому тип:
phone?: string
будет одинаковым независимо от hasPhone.
mixedconst schema = yup.object({
id: yup
.mixed<string>()
.test("uuid", "Invalid UUID", value => {
return /^[0-9a-f-]{36}$/i.test(value || "")
})
.required(),
})
const schema = yup.object({
avatar: yup.mixed<File>().required(),
})
Тип:
{
avatar: File
}
const schema = yup.object({
documents: yup.array(yup.mixed<File>().required()),
})
Тип:
{
documents?: File[]
}
useFormuseForm<TFieldValues>()
type FormData = yup.InferType<typeof schema>
const form = useForm<FormData>({
resolver: yupResolver(schema),
})
SubmitHandlerimport { SubmitHandler } fr om "react-hook-form"
const onSubmit: SubmitHandler<FormData> = data => {
console.log(data)
}
handleSubmit(data => {
data.email
data.age
})
TypeScript автоматически знает структуру объекта.
errors.email?.message
Тип:
string | undefined
errors.profile?.firstName?.message
useFieldArrayconst schema = yup.object({
skills: yup.array(
yup.object({
title: yup.string().required(),
})
),
})
type FormData = yup.InferType<typeof schema>
const { control } = useForm<FormData>({
resolver: yupResolver(schema),
})
const { fields, append } = useFieldArray({
control,
name: "skills",
})
const schema = yup.object({
tags: yup.array(yup.string()).nullable(),
})
Тип:
{
tags?: string[] | null
}
optional()yup.string().optional()
Тип:
string | undefined
Но поле всё ещё может существовать в объекте.
const schema = yup.object({
age: yup.number().transform(value => {
return Number(value)
}),
})
TypeScript не меняет тип после transform.
transformyup.string().transform(() => 123)
Тип всё равно останется:
string
Yup не выводит новый тип после преобразования.
strictyup.number()
Допускает:
"25"
С автоматическим преобразованием.
yup.number().strict()
Теперь строка вызовет ошибку валидации.
Yup плохо поддерживает настоящие union-типы.
const schema = yup.object({
type: yup.mixed<"card" | "cash">()
.oneOf(["card", "cash"])
.required(),
})
lazyconst schema = yup.lazy(value => {
if (typeof value === "string") {
return yup.string()
}
return yup.number()
})
lazyTypeScript обычно выводит:
any
или слишком широкий тип.
const schema = yup.object({
email: yup.string().test(
"email-check",
"Email already exists",
async value => {
const response = await fetch("/api/check-email")
const result = await response.json()
return result.valid
}
),
})
declare module "yup" {
interface StringSchema {
isStrongPassword(): StringSchema
}
}
yup.addMethod(yup.string, "isStrongPassword", function () {
return this.test(
"strong-password",
"Weak password",
value => {
return /[A-Z]/.test(value || "")
}
)
})
const schema = yup.object({
password: yup.string().isStrongPassword().required(),
})
TypeScript понимает новый метод без ошибок.
import * as yup from "yup"
import { yupResolver } from "@hookform/resolvers/yup"
import { useForm } from "react-hook-form"
const schema = yup.object({
email: yup.string().email().required(),
age: yup.number()
.required()
.min(18),
profile: yup.object({
firstName: yup.string().required(),
lastName: yup.string().required(),
}),
roles: yup.array(
yup.string().required()
).required(),
})
type FormData = yup.InferType<typeof schema>
export function App() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: yupResolver(schema),
defaultValues: {
email: "",
age: 18,
profile: {
firstName: "",
lastName: "",
},
roles: [],
},
})
const onSub mit = (data: FormData) => {
console.log(data)
}
return (
<form onSub mit={handleSubmit(onSubmit)}>
<input {...register("email")} />
<input
type="number"
{...register("age", {
valueAsNumber: true,
})}
/>
<input {...register("profile.firstName")} />
<input {...register("profile.lastName")} />
<button type="submit">
Save
</button>
</form>
)
}
InferType как основной источник типовtype FormData = yup.InferType<typeof schema>
Плохо:
interface FormData {
email: string
}
и отдельно:
yup.object({
email: yup.string()
})
strict() для критичных данныхОсобенно:
valueAsNumberДля всех числовых полей формы.
Yup ориентирован на объектные структуры, а не на
дискриминированные union-типы.
transformTypeScript не отслеживает изменение типа после трансформации.
nullable() только при необходимостиИначе типы быстро становятся перегруженными:
string | null | undefined
HTML-элементы почти всегда возвращают строки:
input[type=number]input[type=date]selectДаже если TypeScript ожидает:
number
Date
boolean