Дженерики и утилитарные типы

Библиотека @hookform/resolvers предоставляет адаптеры между системами валидации и React Hook Form. Для схем Yup используется yupResolver, который позволяет:

  • валидировать данные формы через Yup;
  • автоматически выводить типы;
  • синхронизировать типы формы и схемы;
  • использовать дженерики TypeScript;
  • строить безопасные утилитарные типы.

Базовый пример:

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-валидацией. При использовании дженериков появляется полноценная статическая проверка типов.


Типизация схем через InferType

Автоматическое получение типа из схемы

Yup содержит встроенный утилитарный тип 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.


Использование дженериков в useForm

Базовая типизация формы

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

Теперь:

  • register
  • watch
  • setValue
  • getValues
  • handleSubmit

получают строгую типизацию.

Пример:

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

Такой подход опасен:

  • типы могут разъехаться;
  • схема и интерфейс начинают конфликтовать;
  • runtime и compile-time проверки становятся разными.

Правильный подход:

type FormData = yup.InferType<typeof schema>

Источник истины должен быть один.


Дженерики в ObjectSchema

Явное указание типа схемы

Yup позволяет параметризовать схему:

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 и Optional типы

Работа 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
  }[]
}

Глубокая типизация вложенных структур

Nested Object Types

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 на уровне типов.


Устранение 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
}

Enum-подобные структуры

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-схемах.

Conditional Types в Yup

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

const 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

Основные проблемы:

  • слабая поддержка discriminated unions;
  • нет полноценного вывода conditional types;
  • сложность recursive-схем;
  • nullable/optional иногда выводятся неоднозначно;
  • runtime и compile-time типы могут расходиться.

Из-за этого многие крупные проекты переходят на:

  • Zod
  • Valibot
  • ArkType

Но Yup остается крайне распространенным благодаря:

  • зрелости;
  • стабильности;
  • совместимости с React Hook Form;
  • простоте миграции.

Типизация resolver

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

const resolver = yupResolver<FormData>(schema)

Используется редко, потому что InferType обычно достаточно.


Утилитарные типы для форм

Partial

type PartialForm = Partial<FormData>

Все поля становятся optional:

{
  name?: string
  age?: number
}

Полезно для:

  • PATCH-запросов;
  • draft-состояний;
  • пошаговых форм.

Required

type StrictForm = Required<FormData>

Все поля обязательны.


Pick

type Credentials = Pick<FormData, "email" | "password">

Omit

type PublicUser = Omit<FormData, "password">

Record

Динамические ключи

type Errors = Record<string, string>

Пример:

const errors: Errors = {
  email: "Invalid email",
  password: "Too short",
}

Комбинация utility types

type UpdatePayload =
  Partial<Omit<User, "id">>

Очень распространенный паттерн для API.


DeepPartial

Стандартный Partial работает только поверхностно.

type User = {
  profile: {
    name: string
  }
}

Partial<User>:

{
  profile?: {
    name: string
  }
}

Но name остается обязательным.


Реализация DeepPartial

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object
    ? DeepPartial<T[K]>
    : T[K]
}

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

type Draft = DeepPartial<FormData>

Readonly типы

Immutable формы

type ImmutableForm = Readonly<FormData>

Теперь:

form.name = "Alex"

вызывает ошибку.


ReadonlyArray

type Data = {
  tags: ReadonlyArray<string>
}

Запрещено:

tags.push("new")

keyof

Получение ключей формы

type Keys = keyof FormData

Результат:

"name" | "age"

Generic helpers для форм

Универсальная функция обновления

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")

Generic constraints

Ограничение generic-типа

function printId<
  T extends { id: number }
>(value: T) {
  console.log(value.id)
}

Маппинг типов формы

Преобразование всех полей в nullable

type Nullable<T> = {
  [K in keyof T]: T[K] | null
}

Пример

type NullableUser = Nullable<User>

Результат:

{
  id: number | null
  name: string | null
}

NonNullable

Удаление null и undefined

type CleanName =
  NonNullable<string | null | undefined>

Результат:

string

Extract

Извлечение части union

type Role =
  | "admin"
  | "user"
  | "guest"

type AdminRole =
  Extract<Role, "admin">

Exclude

type PublicRole =
  Exclude<Role, "admin">

Результат:

"user" | "guest"

ReturnType

Получение типа функции

function createSchema() {
  return yup.object({
    name: yup.string().required(),
  })
}

type Schema =
  ReturnType<typeof createSchema>

Parameters

Типы аргументов функции

type Args =
  Parameters<typeof updateField>

InstanceType

class ApiClient {
  request() {}
}

type Client =
  InstanceType<typeof ApiClient>

Template Literal Types

Генерация путей полей

type Fields = "email" | "password"

type FormFieldNames =
  `form.${Fields}`

Результат:

"form.email" | "form.password"

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

FieldErrors

В React Hook Form:

import { FieldErrors } from "react-hook-form"

type Errors = FieldErrors<FormData>

Типизация watch

const value = watch("email")

TypeScript автоматически понимает:

string

Типизация setValue

setValue("age", 42)

Ошибка:

setValue("age", "wrong")

Generic-компоненты формы

Универсальный input-компонент

type Props<T> = {
  name: keyof T
}

function Field<T>({
  name
}: Props<T>) {
  return <input />
}

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

React Hook Form предоставляет тип Path.

import { Path } from "react-hook-form"

type Props<T> = {
  name: Path<T>
}

Теперь поддерживаются вложенные пути:

"profile.email"

Типобезопасность nested paths

type Form = {
  profile: {
    contacts: {
      email: string
    }
  }
}

Допустимо:

"profile.contacts.email"

Ошибка:

"profile.unknown"

Generic form factory

Создание универсальной формы

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)

Интеграция DTO и Yup

Совмещение backend-типов и схем

interface CreateUserDto {
  email: string
  password: string
}
const schema:
  yup.ObjectSchema<CreateUserDto> =
    yup.object({
      email: yup.string().required(),
      password: yup.string().required(),
    })

Это предотвращает рассинхронизацию API и frontend.


Runtime и compile-time безопасность

Yup обеспечивает:

  • runtime validation;
  • преобразование данных;
  • parsing;
  • sanitization.

TypeScript обеспечивает:

  • compile-time checking;
  • IDE autocomplete;
  • refactoring safety;
  • контроль generic constraints.

Совместное использование YupResolver и TypeScript позволяет строить строго типизированные формы с минимальным количеством runtime-ошибок.