Рекурсивные схемы применяются для описания структур данных, которые содержат самих себя в определении. Подобные структуры встречаются в деревьях, графах, AST-деревьях, вложенных конфигурациях, JSON-подобных объектах и любых иерархических моделях, где глубина заранее неизвестна.
Валидация таких структур требует механизма, позволяющего ссылаться на схему внутри самой себя без немедленного вычисления. В Superstruct это реализуется через отложенное определение (lazy evaluation), обеспечивающее корректную обработку циклических зависимостей.
Рекурсивная схема — это описание типа, который включает в себя экземпляры самого себя.
Простейший пример — дерево:
Математически это выражается как:
Node = { value, children: Node[] }
Однако прямое определение в рантайме приводит к циклической ссылке, которую нельзя вычислить немедленно. Именно поэтому требуется отложенное разрешение схемы.
В Superstruct рекурсивные структуры создаются с помощью функции
lazy, которая откладывает вычисление структуры до момента
валидации.
Ключевая идея:
Это разрывает цикл определения.
Пример базового использования:
import { object, array, string, lazy } from "superstruct";
const Tree = lazy(() =>
object({
value: string(),
children: array(Tree),
})
);
Здесь Tree ссылается на себя, но фактическое разрешение
происходит только во время выполнения валидации.
Попытка описать рекурсивную структуру напрямую приводит к ошибке:
const Tree = object({
value: string(),
children: array(Tree), // Tree ещё не определён полностью
});
На момент выполнения интерпретатора переменная Tree не
содержит завершённой схемы, что делает невозможным её использование
внутри собственного определения.
lazy решает эту проблему, перенося вычисление внутрь
функции.
const Node = lazy(() =>
object({
id: string(),
children: array(Node),
})
);
Такая структура используется в:
Рекурсивные схемы могут комбинироваться с ограничениями:
const Tree = lazy(() =>
object({
value: string(),
children: array(Tree),
})
);
Добавление ограничений:
const LimitedTree = lazy(() =>
object({
value: string(),
children: array(Tree),
})
);
Ограничения глубины обычно реализуются вне схемы, в логике приложения.
Односвязный список — классический пример рекурсии:
nullconst List = lazy(() =>
object({
value: string(),
next: union([List, null]),
})
);
Здесь используется union, позволяющий завершать
рекурсию.
JSON является естественно рекурсивным форматом:
const JSONValue = lazy(() =>
union([
string(),
number(),
boolean(),
null,
array(JSONValue),
object({
[string()]: JSONValue,
}),
])
);
Подобное определение используется в:
Рекурсивные схемы часто применяются для описания AST:
const Expression = lazy(() =>
union([
object({
type: literal("number"),
value: number(),
}),
object({
type: literal("add"),
left: Expression,
right: Expression,
}),
])
);
Такая модель позволяет описывать выражения произвольной сложности:
(1 + (2 + 3))
Каждый узел ссылается на подвыражения того же типа.
Рекурсия может быть не только самоссылочной, но и взаимной.
Пример: выражения и операнды
const Expression = lazy(() =>
union([NumberNode, AddNode])
);
const NumberNode = object({
type: literal("number"),
value: number(),
});
const AddNode = object({
type: literal("add"),
left: Expression,
right: Expression,
});
Здесь Expression объединяет несколько схем, одна из
которых ссылается обратно на Expression.
Комбинация lazy и union является основным
инструментом построения сложных структур.
Пример дерева с листьями и ветвями:
const Tree = lazy(() =>
union([
object({
type: literal("leaf"),
value: string(),
}),
object({
type: literal("node"),
children: array(Tree),
}),
])
);
Такая модель позволяет:
Использование lazy накладывает определённые
особенности:
Важно учитывать, что рекурсия должна иметь базовый случай:
nullБез базового случая структура становится бесконечной.
Рекурсивность может сочетаться с динамическими ключами:
const Tree = lazy(() =>
object({
value: string(),
children: array(Tree),
})
);
Расширенный вариант с индексированием:
const Forest = lazy(() =>
object({
[string()]: Tree,
})
);
Такие схемы применяются в:
Если схема не содержит терминального значения, рекурсия становится бесконечной.
Возможны ситуации, когда данные сами содержат циклы, но схема не учитывает это:
Superstruct не обрабатывает циклы автоматически, поэтому требуется явная модель завершения.
Рекурсивные структуры являются основой:
Их корректное описание позволяет формализовать сложные структуры без потери гибкости.
Рекурсивные схемы редко существуют изолированно. Обычно они комбинируются:
const Comment = lazy(() =>
object({
id: string(),
text: string(),
replies: array(Comment),
author: object({
name: string(),
id: string(),
}),
})
);
Такая модель типична для:
Рекурсивные схемы позволяют описывать не только структуру, но и поведение данных на уровне формы. Они дают возможность моделировать:
В Superstruct это достигается минимальным набором примитивов:
object, array, union,
lazy.
Комбинирование нескольких рекурсивных типов:
const FileSystem = lazy(() =>
union([
File,
Directory,
])
);
const File = object({
type: literal("file"),
name: string(),
});
const Directory = object({
type: literal("dir"),
name: string(),
children: array(FileSystem),
});
Такая модель полностью описывает файловую систему любой сложности, включая вложенные каталоги.
lazy выполняет роль механизма связывания графа типов.
Без него невозможно:
Он является ключевым элементом, обеспечивающим выразительность системы типов в Superstruct.