Числовые значения

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


Базовая модель числового поля в Yup

Для описания числового значения используется примитив yup.number(). Этот тип задаёт основу для всех дальнейших ограничений и преобразований.

Основные характеристики:

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

Типичная схема начинается с базового определения:

import * as yup from "yup";

const schema = yup.object({
  age: yup.number()
});

Такое определение уже включает базовую проверку на числовой тип, однако не решает проблему строкового ввода.


Преобразование типов и coercion

В контексте HTML-форм значения полей типа <input type="text"> всегда приходят как строки. Даже при использовании type="number" браузер возвращает строковое представление числа.

Yup применяет встроенное преобразование:

  • строка "42" преобразуется в 42;
  • пустая строка "" рассматривается как NaN;
  • нечисловые строки приводят к ошибке или NaN в зависимости от конфигурации.

Для управления поведением преобразования применяется метод transform:

age: yup
  .number()
  .transform((value, originalValue) => {
    return originalValue === "" ? undefined : value;
  })

Такой подход позволяет избежать ситуации, когда пустое поле интерпретируется как 0 или NaN.


Обязательные числовые значения

Числовые поля часто должны быть обязательными. В Yup это задаётся через required().

age: yup
  .number()
  .required()

Однако при работе с формами возникает особенность: 0 является валидным числом, но может быть ошибочно воспринят как пустое значение в пользовательской логике. Поэтому различие между undefined, null и 0 становится критическим.

Для точного контроля используется комбинация:

age: yup
  .number()
  .typeError("Значение должно быть числом")
  .required("Поле обязательно")

typeError позволяет отделить ошибку типа от ошибки обязательности.


Обработка NaN и некорректных значений

В Yup некорректные числовые значения часто приводят к NaN. Поведение NaN отличается от стандартных чисел:

  • NaN не проходит проверки min, max, positive;
  • NaN не равен самому себе;
  • требует отдельной обработки через typeError или transform.

Распространённый паттерн очистки:

age: yup
  .number()
  .transform((value, originalValue) => {
    const parsed = Number(originalValue);
    return isNaN(parsed) ? undefined : parsed;
  })

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


Ограничения диапазона: min и max

Числовые значения часто ограничиваются диапазоном допустимых значений. Yup предоставляет методы min() и max().

age: yup
  .number()
  .min(18, "Минимальный возраст 18")
  .max(65, "Максимальный возраст 65")

Особенности поведения:

  • min и max применяются только к валидным числам;
  • NaN автоматически считается невалидным;
  • порядок вызова не влияет на итоговую проверку, но влияет на читаемость.

Для более сложных ограничений диапазон может быть динамическим:

age: yup
  .number()
  .min(yup.ref("minAge"))

Целые числа и дробные значения

Для ограничения числа целыми значениями используется метод integer():

quantity: yup
  .number()
  .integer("Допустимы только целые значения")

Механика проверки основана на сравнении с математическим округлением:

  • 10 — валидно;
  • 10.5 — невалидно;
  • "10" — проходит преобразование и становится валидным;
  • "10.5" — зависит от парсинга и может быть отклонено.

Часто комбинируется с диапазоном:

quantity: yup
  .number()
  .integer()
  .min(1)
  .max(100)

Допустимость отрицательных и положительных значений

Yup предоставляет специализированные методы:

  • positive() — строго больше 0;
  • negative() — строго меньше 0;
  • nonPositive() — меньше или равно 0;
  • nonNegative() — больше или равно 0.

Пример:

balance: yup
  .number()
  .nonNegative("Значение не может быть отрицательным")

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


Значения по умолчанию и nullable поля

Числовые поля часто допускают отсутствие значения. В Yup это решается через nullable() и default().

age: yup
  .number()
  .nullable()
  .default(null)

Различие между undefined и null критично:

  • undefined обычно означает отсутствие поля;
  • null — явное пустое значение.

При интеграции с формами это влияет на поведение reset и начальных значений.


Интеграция YupResolver с числовыми схемами

В связке с react-hook-form используется yupResolver, который преобразует Yup-схему в валидатор формы.

Базовая интеграция:

import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";

const schema = yup.object({
  age: yup.number().required().min(18)
});

const form = useForm({
  resolver: yupResolver(schema)
});

Механизм работы:

  • значения формы передаются в resolver;
  • Yup выполняет преобразование типов;
  • возвращаются ошибки или валидированные данные;
  • react-hook-form синхронизирует состояние формы.

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


Особенности работы с пустыми строками

Пустая строка является ключевым источником некорректной интерпретации чисел. Без трансформации:

  • ""NaN;
  • NaN → ошибка типа.

Распространённая стратегия:

age: yup
  .number()
  .transform((value, originalValue) =>
    originalValue === "" ? undefined : value
  )
  .required()

Такая логика позволяет отделить отсутствие данных от некорректного ввода.


Использование кастомных проверок

Для сложных сценариев применяется test():

price: yup
  .number()
  .test(
    "is-valid-price",
    "Недопустимое значение цены",
    (value) => value >= 0 && Number.isFinite(value)
  )

Кастомные проверки позволяют:

  • учитывать бизнес-логику;
  • комбинировать несколько условий;
  • обходить ограничения стандартных методов.

Вложенные числовые структуры

Числа часто находятся внутри массивов или объектов:

schema = yup.object({
  items: yup.array().of(
    yup.object({
      price: yup.number().min(0),
      quantity: yup.number().integer().min(1)
    })
  )
});

При работе через YupResolver такие структуры валидируются рекурсивно, что обеспечивает согласованность данных на всех уровнях вложенности.


Поведение при частичной валидации

При частичном обновлении формы числовые поля могут находиться в промежуточном состоянии:

  • пользователь вводит "1", затем "10";
  • поле временно содержит невалидное значение;
  • resolver фиксирует состояние после каждого изменения.

Это требует устойчивых схем, не зависящих от промежуточных строковых состояний.


Оптимизация числовой валидации

Для повышения предсказуемости используются следующие приёмы:

  • явная трансформация строк в числа;
  • использование typeError вместо общих сообщений;
  • отказ от неявных значений;
  • минимизация кастомной логики в test() при простых условиях;
  • строгая типизация через integer, positive, min, max.

Такая структура снижает вероятность расхождений между UI и схемой валидации.


Совместимость с TypeScript

При использовании TypeScript Yup схемы могут выводить типы автоматически, однако числовые поля требуют аккуратного определения:

  • number | undefined при отсутствии required;
  • number при обязательных значениях;
  • number | null при nullable().

Несоответствие между схемой и формой приводит к ошибкам вывода типов и некорректной автокомплитации в IDE.