Кратность и остаток от деления

Валидация числовых значений на кратность широко используется при описании бизнес-правил: шаги, интервалы, сетки, ограничения на ввод, дискретные величины. В Zod подобные условия реализуются через комбинацию базовой числовой схемы и пользовательских проверок (refine, superRefine), поскольку встроенного оператора «кратно» нет.

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

import { z } from "zod";

const multipleOfTen = z.number().refine((value) => value % 10 === 0, {
  message: "Число должно быть кратно 10",
});

Операция остатка от деления (%) используется как основной механизм проверки. Если результат равен нулю, число считается кратным указанному делителю.


Параметризация кратности

Кратность часто требуется не к фиксированному значению, а к переменной. В таких случаях создаётся фабрика схем:

import { z } from "zod";

const multipleOf = (divisor) =>
  z.number().refine((value) => value % divisor === 0, {
    message: `Число должно быть кратно ${divisor}`,
  });

const multipleOfFive = multipleOf(5);
const multipleOfTwelve = multipleOf(12);

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


Использование superRefine для расширенной логики

При необходимости учитывать дополнительные условия применяется superRefine, предоставляющий доступ к контексту ошибок:

const schema = z.number().superRefine((value, ctx) => {
  if (value % 3 !== 0) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Число не кратно 3",
    });
  }

  if (value < 0) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Число должно быть положительным",
    });
  }
});

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


Проверка остатка от деления как бизнес-правило

Остаток от деления применяется не только для кратности, но и для классификации значений. Например, разбиение на категории по модулю:

const categoryByModulo = z.number().refine((value) => {
  const remainder = value % 4;
  return remainder === 1 || remainder === 3;
}, {
  message: "Остаток от деления должен быть 1 или 3",
});

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


Комбинирование с преобразованием входных данных

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

const multipleOfSeven = z
  .string()
  .transform((val) => Number(val))
  .refine((value) => value % 7 === 0, {
    message: "Число должно быть кратно 7",
  });

transform выполняет нормализацию данных до применения логики проверки, что особенно важно при работе с формами и внешними API.


Проверка кратности в составе сложных схем

Кратность может быть частью более крупной структуры данных:

const productSchema = z.object({
  quantity: z.number().refine((value) => value % 2 === 0, {
    message: "Количество должно быть кратным 2",
  }),
  price: z.number().positive(),
});

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


Условная кратность в зависимости от контекста

Иногда делитель зависит от другого поля:

const schema = z.object({
  base: z.number(),
  value: z.number(),
}).refine((data) => data.value % data.base === 0, {
  message: "value должно быть кратно base",
});

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


Особенности работы с отрицательными числами

Оператор остатка в JavaScript сохраняет знак делимого, что может влиять на проверку:

(-10) % 3 // -1

Поэтому при строгой математической логике часто нормализуют значение:

const isMultiple = (value, divisor) =>
  Math.abs(value % divisor) === 0;

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


Валидация диапазонов с кратностью

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

const schema = z.number().refine((value) => {
  return value >= 100 && value <= 1000 && value % 50 === 0;
}, {
  message: "Число должно быть в диапазоне 100–1000 и кратно 50",
});

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


Проверка остатка как инструмент дискретизации

Остаток от деления используется для разбиения значений на группы:

const schema = z.number().refine((value) => {
  const group = value % 5;
  return group === 0;
}, {
  message: "Значение должно попадать в допустимую группу",
});

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


Обработка ошибок при модульной проверке

При использовании superRefine возможно более точное указание источника ошибки:

const schema = z.number().superRefine((value, ctx) => {
  if (value % 9 !== 0) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      path: [],
      message: "Нарушена кратность 9",
    });
  }
});

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


Модульная арифметика в схемах валидации

Модульная арифметика становится базовым инструментом для описания циклических ограничений: расписания, повторяющиеся интервалы, сегментация данных. В связке с Zod она реализуется через функциональные проверки, обеспечивая гибкость без необходимости расширения библиотеки специализированными типами.