Числовые значения в 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, формами и внешними источниками данных.
Базовая схема Zod:
import { z } from "zod";
const schema = z.number();
z.number() проверяет, что значение имеет тип
number, но в современных версиях Zod поведение строгое:
NaN и Infinity считаются невалидными значениями для числовой
схемы по умолчанию, так как они нарушают семантику числовых
данных.
Тем не менее, в реальных потоках данных NaN и Infinity могут появляться до валидации (например, после преобразований).
Для строгой валидации используется метод:
const schema = z.number().finite();
.finite() гарантирует:
NaNInfinity-InfinityЭквивалентная логика:
Number.isFinite(value)
Это наиболее надёжный способ защитить систему от некорректных числовых значений.
В некоторых доменах NaN может использоваться как маркер
отсутствующего или некорректного результата. Для таких случаев Zod
предоставляет отдельный тип:
const nanSchema = z.nan();
Он валидирует только значение NaN:
nanSchema.parse(NaN); // ok
nanSchema.parse(0); // ошибка
Важно, что это не часть числового диапазона, а отдельный примитивный валидатор.
Если требуется допустить и обычные числа, и NaN:
const schema = z.union([z.number(), z.nan()]);
Такой подход применяется редко, поскольку NaN ухудшает предсказуемость вычислений и усложняет агрегации.
Иногда требуется более тонкая логика, чем .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 с бизнес-ограничениями.
При использовании:
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 опасен тем, что:
Поэтому в большинстве прикладных схем применяется строгая стратегия:
const schema = z.number().finite();
или ещё более жёсткая форма:
const schema = z.number().int().finite();
Infinity часто возникает как результат переполнения расчётов:
Валидация через Zod обычно блокирует такие значения:
const schema = z.number().refine(Number.isFinite);
При необходимости допускаются специальные маркеры:
const schema = z.union([
z.number().finite(),
z.literal(Infinity),
z.literal(-Infinity)
]);
Однако такие схемы требуют строгой дисциплины обработки.
В практических схемах Zod используется несколько устойчивых подходов:
Полное исключение специальных значений
z.number().finite()Явное выделение NaN
z.nan()Бизнес-валидация через refine
z.number().refine(Number.isFinite)Контролируемые исключения Infinity
z.union([z.number().finite(), z.literal(Infinity)])Выбор зависит от того, допускается ли потеря математической строгости ради семантики доменной модели.