Конечные и безопасные числа

В JavaScript числовая модель основана на формате IEEE 754 с плавающей точкой двойной точности. Это накладывает фундаментальные ограничения на корректность и предсказуемость работы с числами: существуют значения Infinity, -Infinity, NaN, а также ограничение на безопасный диапазон целых чисел — от -(2^53 - 1) до (2^53 - 1). Эти особенности напрямую влияют на валидацию данных, особенно в контексте серверных приложений и API, где требуется строгая типизация входящих значений.

Конечными числами считаются все значения типа number, которые не являются бесконечностью и не равны NaN. В стандартной модели JavaScript это проверяется через Number.isFinite.

В контексте схем валидации такие ограничения критичны, поскольку Infinity и NaN способны разрушать бизнес-логику:

  • NaN распространяется через арифметические операции
  • Infinity может появляться при делении на ноль
  • сериализация в JSON приводит к потере или искажению значений

В библиотеке Zod конечность числа задаётся через явное ограничение:

import { z } from "zod";

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

Такая схема гарантирует, что значение проходит проверку Number.isFinite(value) и исключает все недопустимые числовые состояния.

Отличие конечных чисел от обычной проверки number

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

z.number()

разрешает любые значения типа number, включая:

  • NaN
  • Infinity
  • -Infinity

Это связано с тем, что JavaScript формально считает их числовым типом.

Использование .finite() добавляет строгую семантику:

z.number().finite()

эквивалентно логике:

(value) => typeof value === "number" && Number.isFinite(value)

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

Безопасные целые числа и предел точности

JavaScript может безопасно представлять только целые числа в диапазоне:

Number.MIN_SAFE_INTEGER === -(2^53 - 1)
Number.MAX_SAFE_INTEGER ===  (2^53 - 1)

За пределами этого диапазона арифметика теряет точность:

9007199254740992 === 9007199254740993 // true

Такая потеря точности делает сравнения и идентификаторы ненадёжными.

В Zod для этого используется ограничение безопасных чисел:

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

или более явно:

z.number().int().safe();

Внутренне это соответствует проверке:

Number.isSafeInteger(value)

Разделение понятий: integer, finite и safe

Эти ограничения относятся к разным аспектам числовой модели:

1. Конечность

z.number().finite()

Отсекает:

  • NaN
  • Infinity
  • -Infinity

Но допускает дробные числа.

2. Целочисленность

z.number().int()

Гарантирует отсутствие дробной части, но не защищает от выхода за пределы безопасного диапазона.

3. Безопасность представления

z.number().safe()

Гарантирует, что число:

  • целое
  • находится в безопасном диапазоне IEEE 754

Комбинирование ограничений

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

const idSchema = z.number().int().safe().positive();

Такой подход обеспечивает:

  • отсутствие дробной части
  • отсутствие потери точности
  • положительное значение

Другой пример для измерений:

const metricSchema = z.number().finite().nonnegative();

Здесь допускаются дробные значения, но исключаются некорректные состояния вычислений.

Особенности NaN в валидации

NaN имеет уникальное поведение:

  • typeof NaN === "number"
  • NaN !== NaN

Это делает его особенно опасным в системах валидации.

Схема:

z.number()

не исключает NaN.

Схема:

z.number().finite()

исключает его явно.

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

BigInt как альтернатива безопасным числам

Для случаев, выходящих за пределы Number.MAX_SAFE_INTEGER, используется BigInt:

const schema = z.bigint();

Особенности:

  • не имеет ограничения точности IEEE 754
  • не совместим напрямую с number
  • не поддерживает дробные значения

Сравнение подходов:

Тип Диапазон Дробные Потеря точности
number ограничен да да
safe integer ограничен нет нет
bigint неограничен нет нет

Практическая роль ограничений в схемах Zod

Комбинация .finite() и .safe() формирует разные уровни строгости:

  • finite — контроль корректности вычислений
  • safe — контроль точности представления
  • int — контроль структуры значения

Валидация входных данных без этих ограничений приводит к накоплению ошибок на уровне бизнес-логики, особенно при:

  • агрегации данных
  • финансовых расчётах
  • идентификаторах сущностей
  • статистических вычислениях

Приведение и трансформации чисел

Zod допускает нормализацию значений перед проверкой:

const schema = z.preprocess((val) => {
  if (typeof val === "string") return Number(val);
  return val;
}, z.number().finite());

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

Составные ограничения и поведение ошибок

При нарушении условий Zod возвращает структурированную ошибку, содержащую:

  • тип нарушения (invalid_type, too_big, too_small)
  • путь до значения
  • ожидаемый тип

Для чисел с ограничением конечности тип ошибки фиксируется как нарушение числового диапазона, поскольку NaN и Infinity рассматриваются как выход за допустимые пределы значений.

Итоговая модель числовой строгости

Внутри Zod числовая система ограничений формирует многоуровневую модель:

  • базовый уровень — number
  • уровень корректности — finite
  • уровень структурной целостности — int
  • уровень точности — safe

Эти уровни позволяют строить строгие схемы, соответствующие математически корректному представлению данных в условиях ограничений IEEE 754 и особенностей JavaScript-движков.