Generic схемы

Yup предоставляет систему схем валидации, где типы и правила описываются декларативно. При построении сложных приложений возникает необходимость создавать универсальные (generic) схемы, которые могут переиспользоваться, параметризоваться и адаптироваться под различные структуры данных без дублирования логики.

Generic подход в схемах Yup выражается не через классические generics языка JavaScript, а через комбинацию фабрик схем, композиции, отложенного вычисления и возможностей TypeScript-интеграции.


Базовая идея универсальных схем

Generic схема — это схема, которая:

  • принимает параметры (явно или неявно),
  • возвращает настроенную схему Yup,
  • может быть повторно использована в разных контекстах,
  • сохраняет единый набор правил валидации.

В основе лежит функция-фабрика:

import * as yup from "yup";

const createStringSchema = (min = 0, max = 255) => {
  return yup.string().min(min).max(max).required();
};

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


Параметризация через фабрики схем

Фабрики — основной способ создания generic-схем в Yup. Они позволяют инкапсулировать правила и управлять ими через аргументы.

Пример числовой схемы с параметрами

const createNumberSchema = ({ min, max, integer = false }) => {
  let schema = yup.number();

  if (integer) {
    schema = schema.integer();
  }

  if (min !== undefined) {
    schema = schema.min(min);
  }

  if (max !== undefined) {
    schema = schema.max(max);
  }

  return schema.required();
};

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


Generic схемы для объектов

Объектные схемы являются частым источником дублирования. Универсализация достигается через функцию, принимающую описание структуры.

const createUserSchema = ({ emailRequired = true }) => {
  return yup.object({
    name: yup.string().required(),
    email: emailRequired ? yup.string().email().required() : yup.string().email(),
    age: yup.number().min(0),
  });
};

При расширении структуры можно комбинировать схемы.


Композиция схем через concat

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

const baseString = yup.string().trim();

const emailSchema = baseString.concat(
  yup.string().email("Некорректный email")
);

Это позволяет создавать слои абстракции: базовая схема → специализированная схема → доменная схема.


Lazy схемы и динамическая типизация

Yup предоставляет механизм lazy, который позволяет определять схему во время выполнения. Это ключевой инструмент для generic логики.

Использование lazy

const dynamicSchema = 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();
});

Lazy-схемы позволяют строить полиморфные структуры данных, где тип значения определяется в runtime.


Discriminated union через lazy

Хотя Yup не предоставляет полноценные union-типы, их можно эмулировать через discriminator-поля.

const schema = yup.lazy((obj) => {
  switch (obj?.type) {
    case "admin":
      return yup.object({
        type: yup.string().oneOf(["admin"]),
        permissions: yup.array().required(),
      });

    case "user":
      return yup.object({
        type: yup.string().oneOf(["user"]),
        email: yup.string().email().required(),
      });

    default:
      return yup.mixed().test("invalid-type", "Unknown type", () => false);
  }
});

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


Generic схемы и TypeScript-интеграция

При использовании TypeScript Yup может работать с типами через inferencing и явные generics.

Infer типов схем

import * as yup from "yup";

const userSchema = yup.object({
  name: yup.string().required(),
  age: yup.number().required(),
});

type User = yup.InferType<typeof userSchema>;

Generic фабрики с типами

const createArraySchema = <T>(itemSchema: yup.Schema<T>) => {
  return yup.array().of(itemSchema);
};

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


Переиспользуемые базовые схемы

Часто создаются стандартные primitive-схемы, которые затем комбинируются.

const schemas = {
  id: yup.string().uuid().required(),
  createdAt: yup.date().required(),
  email: yup.string().email().required(),
};

Затем они используются как строительные блоки:

const postSchema = yup.object({
  id: schemas.id,
  authorEmail: schemas.email,
  createdAt: schemas.createdAt,
  title: yup.string().required(),
});

Абстракции над схемами

Для повышения переиспользуемости создаются функции-обёртки:

const withRequired = (schema) => schema.required();

const withMinLength = (schema, length) => schema.min(length);

Комбинирование таких функций формирует декларативный DSL поверх Yup.


Динамические объектные схемы

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

const createDynamicObjectSchema = (shapeFactory) => {
  return yup.object().shape(shapeFactory());
};

Пример использования:

const schema = createDynamicObjectSchema(() => ({
  [fieldName]: yup.string().required(),
}));

Условные generic схемы

Метод when позволяет параметризовать схему в зависимости от других полей.

const schema = yup.object({
  role: yup.string().required(),
  accessCode: yup.string().when("role", {
    is: "admin",
    then: (s) => s.required(),
    otherwise: (s) => s.notRequired(),
  }),
});

Это формирует зависимые generic-правила внутри одной структуры.


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

Yup позволяет расширять API через addMethod, что также используется для создания generic-расширений.

yup.addMethod(yup.string, "startsWithA", function () {
  return this.test("starts-with-a", "Must start with A", (value) => {
    return value?.startsWith("A");
  });
});

После расширения метод становится частью всех строковых схем.


Архитектурный подход к generic схемам

При масштабировании схемы начинают формироваться слои:

  • primitive layer — базовые string/number/date схемы
  • factory layer — функции генерации схем
  • composition layer — concat, object, array комбинации
  • dynamic layer — lazy и conditional схемы
  • domain layer — бизнес-специфичные схемы

Такое разделение снижает связность и повышает повторное использование.


Ограничения generic подхода в Yup

Несмотря на гибкость, существуют ограничения:

  • отсутствие полноценной параметризации типов на уровне runtime
  • сложность типизации сложных lazy-структур
  • рост сложности при глубокой вложенности фабрик
  • ограниченная поддержка union/discriminated типов без ручной логики

Эти ограничения компенсируются архитектурными паттернами и TypeScript-слоем.