Библиотека @hookform/resolvers предоставляет адаптеры
между системами валидации и React Hook Form. Для схем Yup используется
yupResolver, который позволяет:
Базовый пример:
import { useForm } from "react-hook-form"
import { yupResolver } from "@hookform/resolvers/yup"
import * as yup from "yup"
const schema = yup.object({
email: yup.string().email().required(),
age: yup.number().required(),
})
const form = useForm({
resolver: yupResolver(schema),
})
Без TypeScript такой код ограничивается runtime-валидацией. При использовании дженериков появляется полноценная статическая проверка типов.
InferTypeYup содержит встроенный утилитарный тип InferType.
const schema = yup.object({
name: yup.string().required(),
age: yup.number().required(),
})
type FormData = yup.InferType<typeof schema>
Полученный тип:
type FormData = {
name: string
age: number
}
Это один из важнейших механизмов интеграции Yup и TypeScript.
useFormconst form = useForm<FormData>({
resolver: yupResolver(schema),
})
Теперь:
registerwatchsetValuegetValueshandleSubmitполучают строгую типизацию.
Пример:
form.setValue("name", "Alex")
Ошибка:
form.setValue("unknown", 123)
TypeScript:
Argument of type '"unknown"' is not assignable
Нередко разработчики пишут:
type FormData = {
name: string
age: number
}
и отдельно:
const schema = yup.object({
name: yup.string().required(),
age: yup.number().required(),
})
Такой подход опасен:
Правильный подход:
type FormData = yup.InferType<typeof schema>
Источник истины должен быть один.
ObjectSchemaYup позволяет параметризовать схему:
interface User {
name: string
age: number
}
const schema: yup.ObjectSchema<User> = yup.object({
name: yup.string().required(),
age: yup.number().required(),
})
Теперь схема обязана соответствовать интерфейсу.
Ошибка:
const schema: yup.ObjectSchema<User> = yup.object({
name: yup.string().required(),
})
TypeScript сообщит об отсутствии age.
nullable()const schema = yup.object({
middleName: yup.string().nullable(),
})
type Data = yup.InferType<typeof schema>
Результат:
{
middleName: string | null | undefined
}
Причина:
null.required()const schema = yup.object({
middleName: yup.string().nullable().required(),
})
Тип:
{
middleName: string | null
}
undefined исчезает.
defined()const schema = yup.object({
middleName: yup.string().defined(),
})
Тип:
{
middleName: string
}
array()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
}[]
}
const schema = yup.object({
profile: yup.object({
firstName: yup.string().required(),
contacts: yup.object({
email: yup.string().email().required(),
}),
}),
})
Тип:
{
profile?: {
firstName: string
contacts?: {
email: string
}
}
}
default()default влияет на
типconst schema = yup.object({
role: yup.string().default("user"),
})
TypeScript:
{
role?: string
}
Несмотря на default-значение, поле остается optional на уровне типов.
const schema = yup.object({
role: yup.string().default("user").defined(),
})
Теперь:
{
role: string
}
mixed<T>()type UUID = string
const schema = yup.object({
id: yup.mixed<UUID>().required(),
})
Тип:
{
id: UUID
}
type Role = "admin" | "user" | "guest"
const schema = yup.object({
role: yup.mixed<Role>().oneOf(["admin", "user", "guest"]).required(),
})
AssertsКроме InferType, Yup предоставляет
Asserts.
type Data = yup.Asserts<typeof schema>
Обычно:
InferType применяется чаще;Asserts полезен при сложных conditional-схемах.whenconst schema = yup.object({
isAdmin: yup.boolean().required(),
permissions: yup.array(
yup.string().required()
).when("isAdmin", {
is: true,
then: schema => schema.required(),
otherwise: schema => schema.optional(),
}),
})
Типизация здесь ограничена.
TypeScript не способен полностью вывести условные зависимости runtime-схем Yup.
Основные проблемы:
Из-за этого многие крупные проекты переходят на:
Но Yup остается крайне распространенным благодаря:
resolverconst resolver = yupResolver<FormData>(schema)
Используется редко, потому что InferType обычно
достаточно.
type PartialForm = Partial<FormData>
Все поля становятся optional:
{
name?: string
age?: number
}
Полезно для:
type StrictForm = Required<FormData>
Все поля обязательны.
type Credentials = Pick<FormData, "email" | "password">
type PublicUser = Omit<FormData, "password">
type Errors = Record<string, string>
Пример:
const errors: Errors = {
email: "Invalid email",
password: "Too short",
}
type UpdatePayload =
Partial<Omit<User, "id">>
Очень распространенный паттерн для API.
Стандартный Partial работает только поверхностно.
type User = {
profile: {
name: string
}
}
Partial<User>:
{
profile?: {
name: string
}
}
Но name остается обязательным.
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? DeepPartial<T[K]>
: T[K]
}
type Draft = DeepPartial<FormData>
type ImmutableForm = Readonly<FormData>
Теперь:
form.name = "Alex"
вызывает ошибку.
type Data = {
tags: ReadonlyArray<string>
}
Запрещено:
tags.push("new")
type Keys = keyof FormData
Результат:
"name" | "age"
function updateField<
T,
K extends keyof T
>(
obj: T,
key: K,
value: T[K]
) {
obj[key] = value
}
Использование:
updateField(user, "name", "Alex")
Ошибка:
updateField(user, "age", "wrong")
function printId<
T extends { id: number }
>(value: T) {
console.log(value.id)
}
type Nullable<T> = {
[K in keyof T]: T[K] | null
}
type NullableUser = Nullable<User>
Результат:
{
id: number | null
name: string | null
}
null и
undefinedtype CleanName =
NonNullable<string | null | undefined>
Результат:
string
type Role =
| "admin"
| "user"
| "guest"
type AdminRole =
Extract<Role, "admin">
type PublicRole =
Exclude<Role, "admin">
Результат:
"user" | "guest"
function createSchema() {
return yup.object({
name: yup.string().required(),
})
}
type Schema =
ReturnType<typeof createSchema>
type Args =
Parameters<typeof updateField>
class ApiClient {
request() {}
}
type Client =
InstanceType<typeof ApiClient>
type Fields = "email" | "password"
type FormFieldNames =
`form.${Fields}`
Результат:
"form.email" | "form.password"
В React Hook Form:
import { FieldErrors } from "react-hook-form"
type Errors = FieldErrors<FormData>
watchconst value = watch("email")
TypeScript автоматически понимает:
string
setValuesetValue("age", 42)
Ошибка:
setValue("age", "wrong")
type Props<T> = {
name: keyof T
}
function Field<T>({
name
}: Props<T>) {
return <input />
}
PathReact Hook Form предоставляет тип Path.
import { Path } from "react-hook-form"
type Props<T> = {
name: Path<T>
}
Теперь поддерживаются вложенные пути:
"profile.email"
type Form = {
profile: {
contacts: {
email: string
}
}
}
Допустимо:
"profile.contacts.email"
Ошибка:
"profile.unknown"
function createForm<
T extends yup.AnyObject
>(
schema: yup.ObjectSchema<T>
) {
return useForm<T>({
resolver: yupResolver(schema),
})
}
const schema = yup.object({
email: yup.string().required(),
})
const form = createForm(schema)
interface CreateUserDto {
email: string
password: string
}
const schema:
yup.ObjectSchema<CreateUserDto> =
yup.object({
email: yup.string().required(),
password: yup.string().required(),
})
Это предотвращает рассинхронизацию API и frontend.
Yup обеспечивает:
TypeScript обеспечивает:
Совместное использование YupResolver и TypeScript позволяет строить строго типизированные формы с минимальным количеством runtime-ошибок.