Работа с NaN и Infinity

Числовые значения в JavaScript подчиняются стандарту IEEE 754, что приводит к появлению двух специальных состояний: NaN (Not a Number) и Infinity / -Infinity. Эти значения формально относятся к типу number, но ведут себя иначе при сравнении, сериализации и проверке корректности данных. Валидационные схемы в Zod должны учитывать эти особенности, иначе в систему легко проникают некорректные данные.


NaN возникает в результате некорректных математических операций:

Number("abc") // NaN
0 / 0 // NaN

Особенность NaN заключается в том, что он не равен самому себе:

NaN === NaN // false
Number.isNaN(NaN) // true

Infinity и -Infinity появляются при переполнении числового диапазона:

1 / 0 // Infinity
-1 / 0 // -Infinity

Эти значения часто становятся источником ошибок при работе с API, формами и внешними источниками данных.


Поведение z.number() и проблемные значения

Базовая схема Zod:

import { z } from "zod";

const schema = z.number();

z.number() проверяет, что значение имеет тип number, но в современных версиях Zod поведение строгое: NaN и Infinity считаются невалидными значениями для числовой схемы по умолчанию, так как они нарушают семантику числовых данных.

Тем не менее, в реальных потоках данных NaN и Infinity могут появляться до валидации (например, после преобразований).


Явное исключение NaN и Infinity через .finite()

Для строгой валидации используется метод:

const schema = z.number().finite();

.finite() гарантирует:

  • исключение NaN
  • исключение Infinity
  • исключение -Infinity

Эквивалентная логика:

Number.isFinite(value)

Это наиболее надёжный способ защитить систему от некорректных числовых значений.


Разделение NaN как допустимого значения

В некоторых доменах NaN может использоваться как маркер отсутствующего или некорректного результата. Для таких случаев Zod предоставляет отдельный тип:

const nanSchema = z.nan();

Он валидирует только значение NaN:

nanSchema.parse(NaN); // ok
nanSchema.parse(0);   // ошибка

Важно, что это не часть числового диапазона, а отдельный примитивный валидатор.


Комбинирование number и NaN

Если требуется допустить и обычные числа, и NaN:

const schema = z.union([z.number(), z.nan()]);

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


Проверка через refine для контроля допустимых значений

Иногда требуется более тонкая логика, чем .finite():

const schema = z.number().refine((val) => Number.isFinite(val));

Можно расширить условия:

const schema = z.number().refine((val) => {
  return Number.isFinite(val) && val >= 0;
});

refine полезен, когда требуется объединить проверку NaN/Infinity с бизнес-ограничениями.


Проблема coercion и появление NaN

При использовании:

const schema = z.coerce.number();

Zod пытается привести значение к числу:

z.coerce.number().parse("abc"); // NaN

Это критический момент: результатом становится NaN, который может пройти дальше по системе, если не применена дополнительная проверка.

Безопасный вариант:

const schema = z.coerce.number().finite();

Работа с внешними источниками данных

JSON-формат не поддерживает NaN и Infinity. При сериализации такие значения превращаются в null или исключаются:

JSON.stringify({ a: NaN }) // {"a":null}
JSON.stringify({ a: Infinity }) // {"a":null}

Это приводит к неоднозначности на этапе парсинга:

  • null может означать отсутствие значения
  • либо результат некорректного вычисления

Поэтому при использовании Zod важно явно различать:

const schema = z.union([z.number().finite(), z.null()]);

Приведение строковых значений к числам с защитой

При обработке форм часто встречаются строки:

const schema = z.preprocess((val) => {
  if (val === "NaN") return NaN;
  if (val === "Infinity") return Infinity;
  return val;
}, z.number().finite());

Однако такой подход обычно нежелателен, так как создаёт неявные состояния.

Более безопасная стратегия:

const schema = z.coerce.number().finite();

Ограничение распространения NaN в схемах

NaN опасен тем, что:

  • ломает арифметические операции
  • не сериализуется корректно
  • не проходит проверки равенства
  • скрывает ошибки вычислений

Поэтому в большинстве прикладных схем применяется строгая стратегия:

const schema = z.number().finite();

или ещё более жёсткая форма:

const schema = z.number().int().finite();

Поведение Infinity в доменной логике

Infinity часто возникает как результат переполнения расчётов:

  • расчёт процентов при делении на ноль
  • некорректные агрегаты
  • ошибки нормализации данных

Валидация через Zod обычно блокирует такие значения:

const schema = z.number().refine(Number.isFinite);

При необходимости допускаются специальные маркеры:

const schema = z.union([
  z.number().finite(),
  z.literal(Infinity),
  z.literal(-Infinity)
]);

Однако такие схемы требуют строгой дисциплины обработки.


Типовые стратегии обработки NaN и Infinity

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

  1. Полное исключение специальных значений

    z.number().finite()
  2. Явное выделение NaN

    z.nan()
  3. Бизнес-валидация через refine

    z.number().refine(Number.isFinite)
  4. Контролируемые исключения Infinity

    z.union([z.number().finite(), z.literal(Infinity)])

Выбор зависит от того, допускается ли потеря математической строгости ради семантики доменной модели.