Ленивая валидация: lazy schemas

Понятие ленивой схемы

Ленивая схема валидации в Yup реализуется через механизм yup.lazy(), который позволяет отложить создание финальной схемы до момента фактической валидации данных. Это означает, что структура правил не фиксируется заранее, а вычисляется динамически на основе входного значения.

Основной смысл подхода заключается в том, что схема становится функцией от данных:

  • на этапе объявления схема не содержит полной структуры;
  • на этапе валидации схема формируется на основе входного значения;
  • итоговая логика может отличаться для разных типов данных или их содержимого.

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


Базовая сигнатура yup.lazy

Функция yup.lazy() принимает функцию, которая возвращает схему:

Yup.lazy((value) => {
  return schema;
});

Аргумент value — это текущее значение, которое проходит валидацию. На основе этого значения возвращается соответствующая схема.


Динамическая схема на основе типа значения

Один из наиболее частых сценариев использования — выбор схемы в зависимости от типа данных:

import * as Yup from "yup";

const schema = Yup.lazy((value) => {
  if (typeof value === "string") {
    return Yup.string().min(3);
  }

  if (typeof value === "number") {
    return Yup.number().min(10);
  }

  return Yup.mixed().notRequired();
});

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


Валидация объектов с изменяемой структурой

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

const schema = Yup.object({
  type: Yup.string().required(),
  config: Yup.lazy((value, options) => {
    const parent = options.parent;

    if (parent.type === "A") {
      return Yup.object({
        aField: Yup.string().required(),
      });
    }

    if (parent.type === "B") {
      return Yup.object({
        bField: Yup.number().required(),
      });
    }

    return Yup.mixed().notRequired();
  }),
});

В этом примере поле config меняет структуру в зависимости от значения type. Это позволяет описывать полиморфные объекты без необходимости создавать отдельные схемы для каждого варианта.


Работа с массивами разнородных элементов

yup.lazy() особенно полезен при валидации массивов, содержащих элементы разных типов.

const schema = Yup.array().of(
  Yup.lazy((value) => {
    if (typeof value === "string") {
      return Yup.string().min(2);
    }

    if (typeof value === "number") {
      return Yup.number().positive();
    }

    return Yup.mixed();
  })
);

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


Условные схемы вместо when

Хотя Yup предоставляет метод when() для условной логики, lazy() даёт более низкоуровневый контроль.

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

  • when() — декларативная условная логика;
  • lazy() — программное формирование схемы.

Пример, эквивалентный условной логике через lazy:

const schema = Yup.object({
  role: Yup.string(),
  permissions: Yup.lazy((_, options) => {
    const role = options.parent.role;

    switch (role) {
      case "admin":
        return Yup.array().of(Yup.string().required());
      case "user":
        return Yup.array().max(2);
      default:
        return Yup.array().max(0);
    }
  }),
});

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


Ленивые схемы и рекурсивные структуры

Одно из ключевых применений yup.lazy() — рекурсивные данные, например деревья.

const nodeSchema = Yup.lazy(() =>
  Yup.object({
    id: Yup.string().required(),
    children: Yup.array().of(nodeSchema).notRequired(),
  })
);

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


Контекст и доступ к родительским значениям

Функция внутри lazy() получает не только текущее значение, но и контекст:

  • options.parent — родительский объект;
  • options.path — путь к текущему значению;
  • options.originalValue — исходное значение до преобразований.

Это позволяет строить схемы, зависящие от окружения:

const schema = Yup.object({
  mode: Yup.string(),
  value: Yup.lazy((val, options) => {
    const mode = options.parent.mode;

    if (mode === "strict") {
      return Yup.number().required().min(100);
    }

    return Yup.mixed();
  }),
});

Производительность ленивых схем

Ленивая схема создаётся при каждой валидации конкретного значения. Это означает:

  • отсутствует предварительная компиляция структуры;
  • накладные расходы возникают на этапе выполнения;
  • сложные функции внутри lazy() могут влиять на скорость валидации.

В высоконагруженных сценариях важно учитывать:

  • минимизацию логики внутри фабрики схемы;
  • кеширование результатов, если структура повторяется;
  • избегание тяжёлых вычислений внутри lazy().

Типизация и TypeScript

В TypeScript использование yup.lazy() требует явного указания возвращаемого типа:

const schema = Yup.lazy((value: unknown): Yup.AnySchema => {
  if (typeof value === "string") {
    return Yup.string();
  }

  return Yup.number();
});

При более сложных структурах может потребоваться объединение типов через Yup.MixedSchema или кастомные интерфейсы.


Ограничения ленивых схем

Несмотря на гибкость, подход имеет ряд особенностей:

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

В системах с жёсткой типизацией и фиксированными контрактами данных использование lazy() оправдано только в строго ограниченных местах.


Композиция с другими схемами

yup.lazy() не заменяет стандартные схемы, а расширяет их возможности. Он может быть встроен в любые конструкции:

  • Yup.object().shape({})
  • Yup.array().of(...)
  • Yup.mixed()
  • вложенные схемы любой глубины

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