Целые числа и числа с плавающей точкой

В Zod числовой тип строится вокруг базового примитива z.number(), который описывает допустимые значения типа number в JavaScript. В отличие от простого приведения типов в рантайме, схема Zod формирует строгую валидацию входных данных, различая корректные числа и значения, которые лишь внешне похожи на числа.

Ключевая особенность JavaScript-чисел заключается в едином типе для целых и дробных значений. Zod сохраняет эту модель, но добавляет семантические ограничения через цепочку методов:

import { z } from "zod";

const schema = z.number();

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


Ограничение допустимых значений числа

Базовая схема z.number() редко используется без уточнений. Наиболее распространённые ограничения связаны с диапазонами значений:

const age = z.number().min(0).max(120);

Методы:

  • min(value) — минимально допустимое значение
  • max(value) — максимально допустимое значение
  • positive() — строго больше нуля
  • nonnegative() — больше или равно нулю
  • negative() — меньше нуля
  • nonpositive() — меньше или равно нулю

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


Разделение целых и дробных чисел

JavaScript не различает целые и дробные числа на уровне типа, поэтому Zod реализует проверку целочисленности явно:

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

Метод int() проверяет, что число не содержит дробной части. При этом используется строгое сравнение с округлённым значением, что позволяет отсеивать значения вроде 1.5, 3.14, -2.7.

Пример поведения:

  • 10 — проходит валидацию
  • 10.0 — проходит валидацию (эквивалент целого числа)
  • 10.1 — отклоняется

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

const page = z.number().int().min(1);

Поведение NaN и Infinity

JavaScript допускает значения NaN, Infinity и -Infinity, но их использование в прикладной валидации обычно нежелательно.

Zod по умолчанию не пропускает NaN, несмотря на то что он имеет тип number:

z.number().parse(NaN); // ошибка

Однако Infinity и -Infinity проходят базовую проверку z.number(), если не ограничены:

z.number().parse(Infinity); // допустимо

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

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

Метод finite() исключает все специальные значения и допускает только конечные числа.


Точность и числа с плавающей точкой

Числа с плавающей точкой в JavaScript подчиняются стандарту IEEE 754, что приводит к известным проблемам точности:

0.1 + 0.2 !== 0.3

Zod не изменяет это поведение, но позволяет фиксировать допустимые диапазоны и шаги значений через multipleOf:

const price = z.number().multipleOf(0.01);

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


Преобразование строк в числа

В реальных входных данных числа часто приходят в виде строк. Для этого используется механизм приведения типов:

const schema = z.coerce.number();

coerce.number() выполняет автоматическое преобразование:

  • "123"123
  • "3.14"3.14
  • "" → ошибка
  • "abc" → ошибка

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


Композиция ограничений

Схемы чисел часто строятся как комбинация нескольких правил:

const schema = z
  .coerce.number()
  .int()
  .min(1)
  .max(100);

Такое описание задаёт полный контракт:

  • вход может быть строкой или числом
  • значение должно быть целым
  • значение должно находиться в диапазоне 1–100

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


Пользовательские проверки

Когда встроенных ограничений недостаточно, используется refine:

const schema = z.number().refine((val) => val % 2 === 0);

refine позволяет задавать произвольную логику проверки, сохраняя при этом типизацию number. Для более сложных сценариев можно использовать superRefine, позволяющий добавлять детализированные ошибки:

const schema = z.number().superRefine((val, ctx) => {
  if (val < 0) {
    ctx.addIssue({
      code: "custom",
      message: "Число должно быть неотрицательным",
    });
  }
});

Безопасный разбор и ошибки

Zod предоставляет два основных режима обработки:

  • parse — выбрасывает исключение при ошибке
  • safeParse — возвращает структурированный результат
const result = z.number().safeParse("123");

Результат содержит:

  • success: true и data, если значение корректно
  • success: false и error, если проверка не прошла

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


Влияние числовых схем на типизацию TypeScript

Каждая числовая схема в Zod автоматически выводит тип:

const schema = z.number().int();
type T = z.infer<typeof schema>; // number

Однако логические ограничения (например, int, диапазоны, finite) не отражаются в TypeScript-типе, оставаясь исключительно на уровне runtime-валидации. Это создаёт разделение между структурным типом и фактическими ограничениями данных.


Частые комбинации числовых ограничений

На практике числовые схемы часто формируют предсказуемые паттерны:

  • идентификаторы:
z.number().int().positive();
  • процентные значения:
z.number().min(0).max(100);
  • координаты:
z.number().min(-180).max(180);
  • денежные значения:
z.number().finite().nonnegative().multipleOf(0.01);

Каждая комбинация формирует отдельную семантическую модель данных, несмотря на общий примитив number.