Во многих типах данных невозможно заранее полностью описать структуру без самоссылок. Классический пример — дерево, граф или вложенные комментарии, где элемент содержит массив элементов того же типа.
В статически объявляемых схемах возникает проблема циклической зависимости: структура должна ссылаться сама на себя, но при этом ещё не определена в момент объявления.
Для решения этой задачи используется механизм отложенного вычисления схем — lazy-структуры.
В библиотеке Superstruct lazy-структура представляет собой функцию, которая возвращает описание структуры только в момент валидации, а не при её объявлении.
Сигнатура:
lazy(fn)
где fn — функция, возвращающая структуру
(struct).
Ключевая особенность заключается в том, что вызов fn
откладывается до момента проверки значения.
Типичный случай — дерево:
import { struct, array, string, lazy } from 'superstruct'
const Tree = lazy(() => struct({
name: string(),
children: array(Tree),
}))
Здесь Tree ссылается сам на себя через
lazy. Без отложенного вызова такая конструкция привела бы к
ошибке инициализации, поскольку Tree не существует в момент
объявления children.
При проверке значения алгоритм работает следующим образом:
lazy.Это означает, что lazy не влияет на саму систему типов,
а лишь управляет моментом вычисления схемы.
Lazy часто применяется не только к деревьям, но и к любым вложенным типам переменной глубины.
Пример: вложенные комментарии
const Comment = lazy(() => struct({
id: string(),
text: string(),
replies: array(Comment),
}))
Такая модель позволяет описывать структуры любой глубины без ограничений.
Lazy поддерживает и более сложные сценарии, включая взаимную рекурсию нескольких структур.
const Node = lazy(() => struct({
value: string(),
edges: array(Node),
}))
При этом важно учитывать, что графовые структуры могут содержать циклы не только на уровне схемы, но и на уровне данных, что требует аккуратного проектирования валидации.
Lazy позволяет ссылаться не только на себя, но и на другие структуры, которые ещё не определены.
const A = lazy(() => struct({
b: B,
}))
const B = lazy(() => struct({
a: A,
}))
Такой подход используется для описания взаимосвязанных доменных моделей, например, пользователей и групп, документов и ссылок, узлов и связей.
Lazy-структуры добавляют минимальный накладной расход, однако важно учитывать особенности:
fn вызывается при каждой валидации, но
результат обычно кэшируется внутри одной операции;Оптимально применять lazy только там, где существует реальная циклическая зависимость.
В TypeScript lazy сохраняет корректное выведение типов:
const Tree: Struct<TreeNode> = lazy(() => struct({
value: 'string',
children: array(Tree),
}))
Тип TreeNode корректно выводится даже при рекурсивной
ссылке, поскольку типовая система учитывает отложенное вычисление.
Обычная функция-генератор структуры:
const createTree = () => struct({...})
создаёт новую схему при каждом вызове.
Lazy же:
Одной из частых проблем является попытка использовать lazy без реальной необходимости:
const User = lazy(() => struct({
name: string(),
}))
В данном случае lazy избыточен, так как нет рекурсии.
Другая ошибка — создание побочных эффектов внутри функции lazy:
const Tree = lazy(() => {
console.log('init')
return struct({...})
})
Такие конструкции усложняют предсказуемость валидации, поскольку функция может вызываться многократно.
В реальных приложениях lazy часто используется при моделировании:
Пример AST:
const Expression = lazy(() => union([
struct({ type: 'literal', value: string() }),
struct({ type: 'binary', left: Expression, right: Expression }),
]))
Здесь lazy обеспечивает возможность самоссылки внутри union-ветвей.
Ошибки внутри lazy-структур обрабатываются так же, как и в обычных схемах. Однако стек вызовов может содержать дополнительные уровни, связанные с отложенным вызовом функции.
Это важно учитывать при отладке сложных рекурсивных структур, где ошибка может проявляться глубоко внутри дерева.
Несмотря на гибкость, механизм имеет ограничения:
Lazy следует рассматривать как инструмент разрешения циклических зависимостей в схемах, а не как универсальный механизм построения динамических типов.