Валидация данных в Superstruct опирается на декларативное описание структур, однако значительная часть реальных задач требует динамического формирования схем во время выполнения. Это касается сценариев, где форма данных зависит от внешних факторов: конфигурации приложения, входных параметров, результатов API-запросов или сложных взаимосвязей между полями.
Механизмы динамических структур позволяют переходить от статических описаний к вычисляемым схемам, сохраняя при этом строгую типизацию и предсказуемую валидацию.
Статическая схема фиксируется заранее:
import { object, string, number } from "superstruct";
const User = object({
name: string(),
age: number(),
});
Однако в реальных приложениях часто требуется менять правила валидации в зависимости от контекста. Например, минимальная длина строки может зависеть от настроек или роли пользователя.
Для таких случаев используется подход, при котором структура возвращается функцией.
const createUserStruct = (minNameLength) =>
object({
name: string(),
age: number(),
});
На первый взгляд это обычная фабрика схем, но именно этот приём становится фундаментом динамической композиции.
Одним из ключевых инструментов является 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.
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(),
});
}
};
В таких случаях схема строится после определения ключевого поля, что делает валидацию двухэтапной:
На практике динамические схемы редко используются изолированно. Чаще всего они комбинируются.
const Message = lazy(() =>
object({
type: string(),
payload: union([
object({ type: string(), text: string() }),
object({ type: string(), nodes: array(Message) }),
]),
})
);
Здесь одновременно используются:
Такой подход позволяет описывать сложные структуры, например 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 ограничен, концептуально это реализуется через композицию фабрик.
Динамическое создание схем имеет стоимость. Каждый вызов фабрики создаёт новую структуру, что может повлиять на производительность при массовой валидации.
Ключевые оптимизации:
Пример кеширования:
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, так и сложные само-ссылающиеся структуры данных.