Dynamic

Валидация данных в Superstruct опирается на декларативное описание структур, однако значительная часть реальных задач требует динамического формирования схем во время выполнения. Это касается сценариев, где форма данных зависит от внешних факторов: конфигурации приложения, входных параметров, результатов API-запросов или сложных взаимосвязей между полями.

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


Базовый принцип динамических структур

Статическая схема фиксируется заранее:

import { object, string, number } from "superstruct";

const User = object({
  name: string(),
  age: number(),
});

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

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

const createUserStruct = (minNameLength) =>
  object({
    name: string(),
    age: number(),
  });

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


Lazy-структуры и отложенное вычисление

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

import { lazy, object, string } from "superstruct";

const Node = lazy(() =>
  object({
    value: string(),
    next: Node,
  })
);

Здесь схема рекурсивна: узел содержит ссылку на себя же. Без отложенного вычисления такая конструкция была бы невозможна, поскольку переменная Node ещё не определена в момент описания объекта.

Lazy-структуры решают сразу несколько задач:

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

Условная генерация структур

Динамика часто проявляется в зависимости от входных данных. Вместо одной схемы создаётся набор вариантов.

import { object, string, number, union } from "superstruct";

const Admin = object({
  role: string(),
  permissions: string(),
});

const User = object({
  role: string(),
  age: number(),
});

const createPersonStruct = (type) => {
  if (type === "admin") return Admin;
  return User;
};

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


Динамические union-структуры

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

const Struct = union([
  object({ type: string(), value: string() }),
  object({ type: string(), value: number() }),
]);

Однако даже union можно строить динамически:

const createStruct = (allowedTypes) =>
  union(
    allowedTypes.map((t) =>
      object({
        type: string(),
        value: t === "string" ? string() : number(),
      })
    )
  );

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


Генерация схем из конфигурации

Один из наиболее распространённых сценариев — построение структур на основе конфигурационных объектов.

const config = {
  name: "string",
  age: "number",
  email: "string",
};

const buildStruct = (cfg) =>
  object(
    Object.fromEntries(
      Object.entries(cfg).map(([key, type]) => {
        if (type === "string") return [key, string()];
        if (type === "number") return [key, number()];
      })
    )
  );

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


Зависимые поля и вычисляемая структура

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

const createPaymentStruct = (method) => {
  if (method === "card") {
    return object({
      method: string(),
      cardNumber: string(),
    });
  }

  if (method === "crypto") {
    return object({
      method: string(),
      wallet: string(),
    });
  }
};

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

  1. первичное определение типа;
  2. выбор соответствующей структуры.

Комбинирование lazy и фабрик структур

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

const Message = lazy(() =>
  object({
    type: string(),
    payload: union([
      object({ type: string(), text: string() }),
      object({ type: string(), nodes: array(Message) }),
    ]),
  })
);

Здесь одновременно используются:

  • рекурсия;
  • union;
  • отложенное определение.

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


Интеграция динамики с валидацией массивов

Массивы часто требуют динамического определения типа элементов.

import { array, number, string } from "superstruct";

const createArrayStruct = (type) => {
  if (type === "string") return array(string());
  if (type === "number") return array(number());
};

Расширенный вариант может учитывать конфигурацию:

const createArrayStruct = (cfg) =>
  array(cfg.itemType === "string" ? string() : number());

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


Динамическое расширение структур

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

const base = object({
  id: string(),
});

const extendStruct = (extraFields) =>
  object({
    ...base.schema,
    ...extraFields,
  });

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


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

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

Ключевые оптимизации:

  • кеширование результатов фабрик;
  • переиспользование lazy-структур;
  • минимизация пересоздания union-описаний.

Пример кеширования:

const cache = new Map();

const getStruct = (key, factory) => {
  if (!cache.has(key)) {
    cache.set(key, factory());
  }
  return cache.get(key);
};

Ошибки при проектировании динамических схем

Наиболее частые проблемы связаны не с библиотекой, а с архитектурой:

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

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


Композиция как основа расширяемости

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

  • базовые примитивы (string, number);
  • композиционные операторы (object, array, union);
  • отложенные вычисления (lazy);
  • фабрики для контекста выполнения.

Именно сочетание этих механизмов формирует гибкую систему, способную описывать как простые DTO, так и сложные само-ссылающиеся структуры данных.