Optional

Валидация данных в JavaScript часто сталкивается с ситуациями, когда часть структуры объекта может отсутствовать. Библиотека Superstruct предоставляет встроенный механизм для описания таких случаев через опциональные поля, позволяя точно контролировать допустимость undefined и отсутствие ключей без усложнения схем.

Опциональность в Superstruct — это не просто «поле может быть пустым», а строго определённое поведение структуры, при котором значение либо присутствует и валидируется, либо полностью игнорируется как отсутствующее.


Базовая концепция optional

Функция optional() используется для обозначения структуры, которая может отсутствовать или иметь значение undefined, при этом не нарушая правила валидации.

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

const User = object({
  name: string(),
  nickname: optional(string())
})

В этом примере:

  • name — обязательное поле
  • nickname — может отсутствовать полностью или быть undefined
  • если nickname присутствует, оно обязано быть строкой

Поведение при отсутствии значения

Опциональное поле проходит проверку в двух случаях:

  1. Ключ отсутствует в объекте
  2. Значение явно равно undefined
validate({ name: 'Alex' }, User) // валидно
validate({ name: 'Alex', nickname: undefined }, User) // валидно
validate({ name: 'Alex', nickname: 'Al' }, User) // валидно

Однако значение null не считается эквивалентом отсутствия:

validate({ name: 'Alex', nickname: null }, User) // ошибка

Это важное различие: optional() не делает поле nullable.


Optional и nullable: принципиальное различие

Для поддержки null используется отдельный структурный тип nullable().

import { nullable } from 'superstruct'

const User = object({
  nickname: optional(nullable(string()))
})

Теперь допустимы следующие состояния:

  • отсутствует
  • undefined
  • null
  • строка

Вложенные опциональные поля

Опциональность применяется рекурсивно и может использоваться внутри вложенных объектов.

const Profile = object({
  user: object({
    name: string(),
    contacts: optional(object({
      email: string(),
      phone: optional(string())
    }))
  })
})

Здесь:

  • contacts может отсутствовать полностью
  • внутри contacts поле phone также может отсутствовать
  • при наличии структуры проверка выполняется глубоко

Опциональные поля и массивы

Опциональность может применяться к структурам внутри массивов, но не делает сам массив «частично заполненным». Она работает только на уровне элементов структуры.

const Schema = object({
  tags: optional(array(string()))
})

Возможные значения:

  • отсутствует tags
  • tags: undefined
  • tags: ['js', 'validation']

Недопустимо:

  • tags: null
  • tags: [1, 2, 3]

Дефолтные значения и optional

optional() не задаёт значение по умолчанию. Оно лишь описывает допустимость отсутствия данных. Для автоматической подстановки используется композиция с преобразованиями.

const withDefault = (struct, defaultValue) =>
  coerce(optional(struct), (value) =>
    value === undefined ? defaultValue : value
  )

Пример использования:

const Settings = object({
  theme: withDefault(string(), 'light')
})

Теперь:

  • если theme отсутствует → используется 'light'
  • если присутствует → проходит валидацию как строка

Различие между optional и partial

Superstruct не имеет встроенного аналога partial в стиле TypeScript, но optional() часто используется для аналогичного поведения — частичного описания объекта.

const UpdateUser = object({
  name: optional(string()),
  email: optional(string()),
  age: optional(number())
})

Такой объект допускает любое подмножество полей.

Однако важно учитывать: структура остаётся объектом с валидируемыми типами, а не «полностью свободной схемой».


Условная опциональность

Опциональные поля можно комбинировать с union() для описания зависимых структур.

const Payment = object({
  method: union([literal('card'), literal('cash')]),
  cardNumber: optional(string())
})

Однако такая схема не запрещает логически некорректные комбинации. Для этого требуется дополнительная валидация через refine().


Контроль через refine и optional

Опциональные поля часто используются совместно с пользовательскими проверками.

import { refine } from 'superstruct'

const Struct = object({
  password: optional(refine(string(), 'minLength', (v) => v.length >= 8))
})

Поведение:

  • отсутствие password допустимо
  • наличие строки проверяется на длину
  • пустая строка не проходит проверку

Обработка входных данных

При валидации Superstruct не модифицирует входной объект. Это означает:

  • отсутствующие поля не добавляются автоматически
  • undefined не преобразуется в значения
  • структура остаётся неизменной
const [error, value] = validate({ name: 'Alex' }, User)

console.log(value.nickname) // undefined

Optional в TypeScript-типизации

Superstruct тесно интегрируется с TypeScript через вывод типов.

type User = Infer<typeof UserStruct>

Для опциональных полей результат будет:

{
  name: string
  nickname?: string
}

То есть optional() напрямую влияет на типизацию, формируя необязательные свойства.


Типичные ошибки использования optional

1. Ожидание поведения nullable

nickname: optional(string()) // null недопустим

2. Попытка заменить дефолты

optional(string()) // не задаёт значение

3. Использование без учета вложенности

optional(object({ ... })) // объект либо есть, либо отсутствует целиком

Практические паттерны

Конфигурационные объекты

const Config = object({
  host: string(),
  port: optional(number()),
  debug: optional(boolean())
})

API ответы

const Response = object({
  data: object({
    items: array(string()),
    nextPage: optional(number())
  })
})

Формы и частичные обновления

const Patch = object({
  title: optional(string()),
  content: optional(string())
})

Композиция optional с другими структурами

Опциональность хорошо комбинируется с базовыми примитивами Superstruct:

  • string
  • number
  • boolean
  • array
  • object
  • union

При этом optional() всегда работает как внешний слой над структурой, не изменяя её внутреннюю логику.

optional(union([string(), number()]))

Семантика отсутствия значения

В Superstruct отсутствие поля трактуется строго:

  • нет ключа → значение отсутствует
  • undefined → считается отсутствующим только внутри optional()
  • null → отдельное значение, требующее nullable()

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