Генерация типов форм

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

Главная проблема при работе с формами — дублирование типов:

  • интерфейс TypeScript;
  • схема валидации;
  • типы полей формы.

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
}

Использование типа в useForm

const {
  register,
  handleSubmit,
  formState: { errors },
} = useForm<FormData>({
  resolver: yupResolver(schema),
})

Тип формы теперь связан:

  • со схемой;
  • с register;
  • с errors;
  • с handleSubmit.

Автоматический вывод nullable-полей

Nullable

const schema = yup.object({
  middleName: yup.string().nullable(),
})

type FormData = yup.InferType<typeof schema>

Результат:

type FormData = {
  middleName: string | null | undefined
}

Влияние required()

Без required

const schema = yup.object({
  username: yup.string(),
})

Тип:

{
  username?: string | undefined
}

С required

const 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

С литеральным union

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

Enum TypeScript

enum Status {
  Active = "active",
  Blocked = "blocked",
}

Схема

const schema = yup.object({
  status: yup.mixed<Status>()
    .oneOf(Object.values(Status))
    .required(),
})

Тип:

{
  status: Status
}

Типизация через SchemaOf

SchemaOf<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>

Причины:

  • отсутствует дублирование;
  • схема становится главным источником типов;
  • проще поддерживать большие формы;
  • меньше рассинхронизации.

Типизация defaultValues

Неправильный вариант

useForm<FormData>({
  defaultValues: {
    age: "",
  },
})

Ошибка:

Type 'string' is not assignable to type 'number'

Правильный вариант

useForm<FormData>({
  defaultValues: {
    age: 18,
  },
})

Проблема HTML input и number

Даже если поле имеет тип number, браузер возвращает строку.

Пример проблемы

<input type="number" {...register("age")} />

Фактически значение:

"25"

Решение через valueAsNumber

<input
  type="number"
  {...register("age", {
    valueAsNumber: true,
  })}
/>

Теперь:

25

Типизация checkbox

Схема

const schema = yup.object({
  isAdmin: yup.boolean().required(),
})

Тип

{
  isAdmin: boolean
}

Типизация даты

Схема

const schema = yup.object({
  createdAt: yup.date().required(),
})

Тип:

{
  createdAt: Date
}

Преобразование строк в Date

HTML input возвращает строку.

<input type="date" />

Результат:

"2026-01-01"

Использование transform

const schema = yup.object({
  createdAt: yup.date().transform((value, originalValue) => {
    return originalValue ? new Date(originalValue) : value
  }),
})

Генерация типов для условных полей

Использование when

const 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
}

Ограничения TypeScript при when

TypeScript не умеет полноценно выводить условные типы из when.

Поэтому тип:

phone?: string

будет одинаковым независимо от hasPhone.


Кастомные типы через mixed

UUID

const schema = yup.object({
  id: yup
    .mixed<string>()
    .test("uuid", "Invalid UUID", value => {
      return /^[0-9a-f-]{36}$/i.test(value || "")
    })
    .required(),
})

Типизация файлов

File

const schema = yup.object({
  avatar: yup.mixed<File>().required(),
})

Тип:

{
  avatar: File
}

Массив файлов

const schema = yup.object({
  documents: yup.array(yup.mixed<File>().required()),
})

Тип:

{
  documents?: File[]
}

Типизация через generic у useForm

Полная сигнатура

useForm<TFieldValues>()

Пример

type FormData = yup.InferType<typeof schema>

const form = useForm<FormData>({
  resolver: yupResolver(schema),
})

Типизация SubmitHandler

Явная типизация

import { SubmitHandler } fr om "react-hook-form"

const onSubmit: SubmitHandler<FormData> = data => {
  console.log(data)
}

Автоматическая типизация submit

handleSubmit(data => {
  data.email
  data.age
})

TypeScript автоматически знает структуру объекта.


Типизация ошибок формы

Доступ к ошибкам

errors.email?.message

Тип:

string | undefined

Ошибки вложенных объектов

errors.profile?.firstName?.message

Типизация useFieldArray

Схема

const 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",
})

Проблемы nullable-массивов

Схема

const schema = yup.object({
  tags: yup.array(yup.string()).nullable(),
})

Тип:

{
  tags?: string[] | null
}

Осторожность с optional()

yup.string().optional()

Тип:

string | undefined

Но поле всё ещё может существовать в объекте.


Типизация transform

Пример преобразования

const schema = yup.object({
  age: yup.number().transform(value => {
    return Number(value)
  }),
})

TypeScript не меняет тип после transform.


Важная особенность transform

yup.string().transform(() => 123)

Тип всё равно останется:

string

Yup не выводит новый тип после преобразования.


Строгая типизация через strict

Без strict

yup.number()

Допускает:

"25"

С автоматическим преобразованием.


Со strict

yup.number().strict()

Теперь строка вызовет ошибку валидации.


Работа с union-типами

Yup плохо поддерживает настоящие union-типы.

Ограниченный вариант

const schema = yup.object({
  type: yup.mixed<"card" | "cash">()
    .oneOf(["card", "cash"])
    .required(),
})

Lazy-схемы

Использование lazy

const schema = yup.lazy(value => {
  if (typeof value === "string") {
    return yup.string()
  }

  return yup.number()
})

Ограничения типизации lazy

TypeScript обычно выводит:

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
    }
  ),
})

Типизация кастомных методов

Расширение Yup

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() для критичных данных

Особенно:

  • финансы;
  • административные панели;
  • API-конфигурации;
  • права доступа.

Явно указывать valueAsNumber

Для всех числовых полей формы.


Избегать сложных union-схем

Yup ориентирован на объектные структуры, а не на дискриминированные union-типы.


Осторожно использовать transform

TypeScript не отслеживает изменение типа после трансформации.


Использовать nullable() только при необходимости

Иначе типы быстро становятся перегруженными:

string | null | undefined

Следить за расхождением браузерных типов и TypeScript

HTML-элементы почти всегда возвращают строки:

  • input[type=number]
  • input[type=date]
  • select

Даже если TypeScript ожидает:

number
Date
boolean