Валидация данных в JavaScript часто требует не только проверки
структуры входящих значений, но и автоматического заполнения
отсутствующих полей. В таких сценариях используется механизм значений по
умолчанию, который позволяет формировать предсказуемую форму данных ещё
до начала бизнес-логики. В библиотеке Superstruct этот механизм
реализован через обёртку defaulted, обеспечивающую
подстановку значений при отсутствии входных данных или при их частичной
неполноте.
Функция defaulted принимает два аргумента:
Поведение строится вокруг проверки значения на
undefined. Если значение отсутствует, возвращается
дефолтное. Если значение присутствует — оно проходит стандартную
валидацию.
import { object, string, defaulted } from 'superstruct'
const User = object({
name: defaulted(string(), 'Аноним')
})
В данном примере поле name всегда будет иметь строковое
значение. Если входной объект не содержит name, структура
автоматически подставит 'Аноним'.
Важный аспект поведения заключается в различии между
undefined и null. По умолчанию
defaulted реагирует только на отсутствие значения, но не
заменяет явно переданный null.
const Struct = defaulted(string(), 'default')
Struct.create(undefined) // 'default'
Struct.create(null) // null
Такое поведение позволяет сохранять семантическую разницу между “значение не передано” и “значение явно очищено”.
Наиболее распространённый сценарий — применение
defaulted с базовыми типами: строками, числами и булевыми
значениями.
import { number, boolean } from 'superstruct'
const Config = object({
retries: defaulted(number(), 3),
debug: defaulted(boolean(), false)
})
При отсутствии полей структура всегда будет возвращать предсказуемую конфигурацию:
retries → 3debug → falseПри работе со сложными объектами важно понимать, что
defaulted применяется на уровне конкретного поля, а не
глубокой рекурсии всей структуры.
const Profile = object({
settings: defaulted(object({
theme: string(),
notifications: boolean()
}), {
theme: 'light',
notifications: true
})
})
Здесь, если settings отсутствует, будет подставлен весь
объект. Однако если settings частично заполнен,
автоматического глубокого слияния не происходит.
Если входной объект содержит часть полей, defaulted не
выполняет merge по ключам. Он работает только на уровне всей
структуры.
Profile.create({
settings: {
theme: 'dark'
}
})
Результат:
theme → ‘dark’notifications → undefined (не подставится
автоматически)Это ключевая особенность, отличающая defaulted от
механизмов глубокого объединения.
В качестве значения по умолчанию можно передавать функцию. Это полезно для генерации уникальных или вычисляемых значений.
const Timestamped = object({
createdAt: defaulted(() => Date.now(), Date.now())
})
Функция вызывается только при отсутствии значения. Это позволяет избегать пересчёта при каждом создании структуры.
Более корректный вариант с ленивой инициализацией:
const Timestamped = object({
createdAt: defaulted(number(), () => Date.now())
})
defaulted не отключает проверку типов. Сначала
применяется подстановка значения, затем выполняется валидация итогового
результата.
const Age = defaulted(number(), 18)
Age.create(undefined) // 18
Age.create('18') // ошибка валидации
Таким образом, дефолт не является обходом системы типов, а лишь предварительным этапом нормализации данных.
При комбинировании структур порядок обёрток имеет значение. Например:
const StructA = defaulted(number(), 10)
const StructB = coerce(StructA, value => Number(value))
В этом случае сначала применяется defaulted, затем
coerce. Если порядок изменить, поведение может существенно
отличаться.
Для массивов defaulted применяется аналогично другим
типам, но без автоматического заполнения элементов.
const List = object({
items: defaulted(array(string()), [])
})
Если items отсутствует, возвращается пустой массив. Если
массив передан, он не модифицируется и не дополняется значениями по
умолчанию для элементов.
Допускается многослойное использование defaulted, но
важно учитывать, что каждый уровень работает независимо.
const Struct = object({
config: defaulted(
object({
mode: defaulted(string(), 'safe')
}),
{}
)
})
Здесь возможны два уровня подстановки:
config → {};mode внутри config →
'safe'.После применения create результат уже содержит
подставленные значения. Повторная валидация не восстанавливает исходные
“пустые” поля, так как они физически заменены.
const result = Struct.create(undefined)
После выполнения result уже не содержит информации о
том, что значение было дефолтным.
Несмотря на удобство, механизм имеет ряд ограничений:
null, если это не реализовано
отдельно;Одним из ключевых преимуществ является корректная инференция типов.
При использовании defaulted тип результата автоматически
включает значение по умолчанию.
const Port = defaulted(number(), 3000)
// тип: number
TypeScript учитывает, что значение всегда будет присутствовать после создания структуры, даже если оно отсутствовало во входных данных.
На практике defaulted часто применяется для конфигураций
приложений, где требуется гарантированная полнота структуры.
const AppConfig = object({
host: defaulted(string(), 'localhost'),
port: defaulted(number(), 8080),
secure: defaulted(boolean(), false)
})
Такая схема гарантирует, что после валидации конфигурация всегда будет полной и пригодной для использования без дополнительных проверок на существование полей.
При использовании объединений типов defaulted
применяется к результату выбора конкретного варианта, а не ко всем
веткам сразу.
const Value = defaulted(union([string(), number()]), 0)
Значение по умолчанию должно соответствовать одному из допустимых типов, иначе валидация завершится ошибкой.
При использовании функций в качестве дефолта важно учитывать, что функция вызывается каждый раз при создании нового объекта, а не один раз при объявлении схемы.
const Struct = object({
id: defaulted(number(), () => Math.random())
})
Каждый вызов create с отсутствующим id
будет генерировать новое значение.
Механизм можно описать как последовательность шагов:
Эта модель делает defaulted инструментом нормализации
данных, а не просто синтаксическим сахаром над значениями по
умолчанию.