Базовая типизация схем

Типизация схем в связке с YupResolver строится вокруг строгого соответствия между описанием валидационной схемы и типом данных формы. Основная цель — добиться того, чтобы структура данных, приходящих в форму, была предсказуемой на уровне TypeScript, а не только в runtime-валидации.

В основе работы лежит интеграция двух компонентов: схемы валидации из библиотеки Yup и резолвера из экосистемы react-hook-form через пакет resolver’ов, в частности @hookform/resolvers/yup.

Принцип связывания схемы и типа данных

Yup-схема описывает форму данных декларативно, например:

  • какие поля существуют
  • какие типы они имеют
  • какие ограничения накладываются
  • какие значения считаются валидными

TypeScript-тип, в свою очередь, должен отражать эту структуру максимально точно.

Ключевая проблема без типизации — рассинхронизация:

  • схема ожидает одно
  • форма возвращает другое
  • TypeScript не знает о правилах Yup

Решение заключается в том, чтобы выводить тип данных непосредственно из схемы.

Базовая структура Yup-схемы

Схема строится через объектные описания:

  • string()
  • number()
  • boolean()
  • array()
  • object()

Пример логической структуры:

  • user

    • name: string
    • age: number
    • email: string

В Yup это выражается как объектная схема, которая становится источником типов.

Выведение типов из Yup-схемы

Базовый механизм типизации основан на утилитарном типе:

  • InferType<typeof schema> из Yup

Он позволяет автоматически извлечь структуру данных:

  • string schema → string
  • number schema → number
  • optional field → T | undefined
  • nullable → T | null

Таким образом, схема становится единым источником истины.

Интеграция с YupResolver

Резолвер выполняет роль адаптера между:

  • схемой Yup
  • механизмом валидации react-hook-form

Он принимает схему и возвращает функцию, которую форма использует при сабмите и валидации:

  • преобразует значения
  • проверяет соответствие схеме
  • возвращает ошибки в стандартизированном виде

Важно, что сам YupResolver не влияет на TypeScript напрямую, он работает на runtime-уровне, но его использование предполагает строго типизированную схему.

Базовая типизация формы через схему

Тип данных формы связывается с схемой следующим образом:

  • создаётся Yup-схема
  • из неё извлекается тип
  • этот тип передаётся в дженерик формы

Логическая цепочка:

  1. Schema → источник истины
  2. InferType → TypeScript-тип
  3. useForm → типизация формы

Это исключает необходимость ручного описания интерфейсов.

Типизация примитивных полей

Строки

name: yup.string().required()

Результирующий тип:

  • string

Если поле не обязательно:

  • string | undefined

Числа

age: yup.number().required()

Тип:

  • number

Особенность: Yup различает NaN и undefined, поэтому тип может расширяться при неполной валидации.

Булевы значения

isActive: yup.boolean()

Тип:

  • boolean

При отсутствии значения:

  • boolean | undefined

Типизация массивов

Массивы в Yup описываются через array().of(...).

Пример:

tags: yup.array().of(yup.string().required())

Типизация:

  • string[]

Если массив опционален:

  • string[] | undefined

При nullable:

  • string[] | null

Особенность: вложенные схемы полностью рекурсивно типизируются.

Типизация объектов

Объекты являются ключевым элементом типизации схем.

user: yup.object({
  name: yup.string().required(),
  age: yup.number().required()
})

Результат:

{
  name: string;
  age: number;
}

Если объект опционален:

user?: {
  name: string;
  age: number;
}

Вложенные схемы и рекурсия типов

Yup поддерживает глубокую вложенность объектов:

profile: yup.object({
  contacts: yup.object({
    email: yup.string().email().required()
  })
})

Типизация становится рекурсивной:

{
  profile: {
    contacts: {
      email: string;
    };
  };
}

На этом уровне важна согласованность всех вложенных InferType.

Optional, nullable и default значения

Типизация сильно зависит от модификаторов схемы:

optional()

  • поле может отсутствовать
  • тип расширяется через undefined

nullable()

  • допускается null

default()

  • влияет на runtime значение
  • не всегда убирает undefined из типа

Комбинации приводят к следующим итогам:

  • optional → T | undefined
  • nullable → T | null
  • optional + nullable → T | null | undefined

Дискриминируемые структуры

Yup позволяет описывать условные схемы через when.

Пример:

type: yup.mixed().oneOf(['a', 'b']),
data: yup.object().when('type', {
  is: 'a',
  then: yup.object({ value: yup.string() }),
  otherwise: yup.object({ count: yup.number() })
})

Типизация в этом случае становится объединением:

| { type: 'a'; dat a: { value: string } }
| { type: 'b'; dat a: { count: number } }

Такие конструкции требуют внимательного контроля, поскольку автоматическое выведение типов может быть менее точным.

Ограничения базовой типизации

Несмотря на мощь InferType, существуют ограничения:

  • сложные conditional schemas могут терять точность
  • кастомные трансформации transform() не всегда корректно отражаются в типах
  • union-логика в when() может требовать ручной корректировки

В таких случаях типизация дополняется вручную через пересечение или переопределение типов.

Связь типизации и резолвера в runtime

Хотя типизация работает на этапе компиляции, YupResolver действует в runtime:

  • получает сырые данные формы
  • прогоняет их через Yup-схему
  • возвращает результат валидации

Типы при этом служат контрактом:

  • что ожидается на входе
  • что гарантируется после валидации

Несоответствие между ними приводит к логическим ошибкам, которые TypeScript не всегда способен поймать без строгой схемной дисциплины.

Практическая модель базовой типизации

Устойчивый подход строится по принципу:

  1. Схема создаётся первой
  2. Тип извлекается автоматически
  3. Форма параметризуется этим типом
  4. YupResolver подключается к той же схеме

Таким образом обеспечивается единая точка правды, где Yup определяет структуру, а TypeScript лишь отражает её без дублирования описаний.