Default

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


Базовый принцип работы defaulted

Функция defaulted принимает два аргумента:

  • базовую структуру (struct), описывающую тип данных;
  • значение по умолчанию или функцию, возвращающую такое значение.

Поведение строится вокруг проверки значения на undefined. Если значение отсутствует, возвращается дефолтное. Если значение присутствует — оно проходит стандартную валидацию.

import { object, string, defaulted } from 'superstruct'

const User = object({
  name: defaulted(string(), 'Аноним')
})

В данном примере поле name всегда будет иметь строковое значение. Если входной объект не содержит name, структура автоматически подставит 'Аноним'.


Отличие undefined от null

Важный аспект поведения заключается в различии между 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 → 3
  • debug → 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’
  • notificationsundefined (не подставится автоматически)

Это ключевая особенность, отличающая 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

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

const Struct = object({
  config: defaulted(
    object({
      mode: defaulted(string(), 'safe')
    }),
    {}
  )
})

Здесь возможны два уровня подстановки:

  • отсутствие config{};
  • отсутствие mode внутри config'safe'.

Поведение при сериализации и повторной валидации

После применения create результат уже содержит подставленные значения. Повторная валидация не восстанавливает исходные “пустые” поля, так как они физически заменены.

const result = Struct.create(undefined)

После выполнения result уже не содержит информации о том, что значение было дефолтным.


Ограничения механизма defaulted

Несмотря на удобство, механизм имеет ряд ограничений:

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

Типизация в TypeScript

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

const Port = defaulted(number(), 3000)
// тип: number

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


Использование в конфигурационных схемах

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

const AppConfig = object({
  host: defaulted(string(), 'localhost'),
  port: defaulted(number(), 8080),
  secure: defaulted(boolean(), false)
})

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


Поведение в сочетании с union-типами

При использовании объединений типов defaulted применяется к результату выбора конкретного варианта, а не ко всем веткам сразу.

const Value = defaulted(union([string(), number()]), 0)

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


Ленивая инициализация и повторное вычисление

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

const Struct = object({
  id: defaulted(number(), () => Math.random())
})

Каждый вызов create с отсутствующим id будет генерировать новое значение.


Итоговая модель поведения defaulted

Механизм можно описать как последовательность шагов:

  1. Проверка входного значения.
  2. Если значение отсутствует — подстановка дефолта.
  3. Валидация итогового результата.
  4. Возврат нормализованного значения.

Эта модель делает defaulted инструментом нормализации данных, а не просто синтаксическим сахаром над значениями по умолчанию.