Nonempty

Назначение и область применения

Валидация данных в JavaScript часто сводится не только к проверке типа значения, но и к проверке его содержимого. Одной из типичных проблем являются «пустые, но формально корректные» значения: пустые строки, пустые массивы, коллекции без элементов. Они проходят базовые проверки типов, но в реальной бизнес-логике почти всегда считаются ошибочными.

В библиотеке Superstruct подобные ограничения реализуются через композицию структур и специализированные утилиты. Одной из ключевых является nonempty, обеспечивающая инвариант: значение существует и содержит хотя бы один элемент или символ.


Базовая семантика Nonempty

nonempty работает как обёртка над существующей структурой и добавляет дополнительное ограничение:

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

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


Базовый синтаксис

В Superstruct nonempty применяется как функция-обёртка над базовой структурой.

import { string, array, nonempty, object, number } from 'superstruct'

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

const Name = nonempty(string())

const Tags = nonempty(array(string()))

const Scores = nonempty(array(number()))

Каждая такая структура становится более строгой версией базовой.


Проверка строк

Наиболее частый сценарий — контроль строковых значений.

const Username = nonempty(string())

Поведение:

Значение Результат
“alex” валидно
“” ошибка
” ” валидно (пробел — символ)

Важно, что nonempty не выполняет тримминг или проверку на пробельные символы. Он проверяет исключительно длину строки.

Для более строгой логики часто комбинируется с трансформацией:

const TrimmedNonemptyString = nonempty(
  string()
)

с предварительной нормализацией:

const value = input.trim()

Проверка массивов

Для массивов nonempty гарантирует наличие хотя бы одного элемента:

const NonEmptyIds = nonempty(array(number()))

Пример поведения:

Значение Результат
[1, 2, 3] валидно
[42] валидно
[] ошибка

Важно учитывать, что проверка выполняется на уровне массива, а не содержимого. Пустые или невалидные элементы внутри массива не влияют на сам факт его непустоты.


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

Сила nonempty раскрывается в сочетании с другими валидаторами Superstruct.

Сложные массивы объектов

const User = object({
  id: number(),
  name: nonempty(string())
})

const Users = nonempty(array(User))

Здесь гарантируется:

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

Поведение ошибок

При нарушении ограничения nonempty возвращает стандартную ошибку Superstruct.

Пример:

import { assert } from 'superstruct'

assert([], nonempty(array(string())))

Результат ошибки:

Expected a nonempty array, but received []

Для строк:

Expected a nonempty string, but received ""

Ошибки формируются на уровне структуры-обёртки, что позволяет точно локализовать проблему.


Использование с optional и default

nonempty может конфликтовать с концепцией необязательных значений, поэтому важно различать:

  • отсутствие значения (undefined)
  • пустое значение ("", [])

Optional

import { optional } from 'superstruct'

const Name = optional(nonempty(string()))

Такое определение означает:

  • значение может отсутствовать;
  • если присутствует — обязано быть непустым.

Default

import { defaulted } from 'superstruct'

const Tags = defaulted(nonempty(array(string())), [])

Здесь возникает логическое противоречие: default [] конфликтует с nonempty. В результате такая конструкция часто используется с осторожностью, либо заменяется на более осмысленную:

const Tags = nonempty(array(string()))

без дефолта, с явной обязательностью данных.


Вложенные структуры и Nonempty

При работе со сложными объектами важно понимать границы применения.

const Post = object({
  title: nonempty(string()),
  comments: nonempty(array(
    object({
      text: nonempty(string())
    })
  ))
})

Логика проверки:

  • title не может быть пустым;
  • comments обязаны содержать хотя бы один элемент;
  • каждый комментарий обязан содержать непустой текст.

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


Отличие от ручных проверок

Без Superstruct аналогичная логика выглядела бы так:

if (!array.length) throw new Error('empty array')
if (!string.length) throw new Error('empty string')

Проблема такого подхода:

  • отсутствие декларативности;
  • дублирование логики;
  • сложность композиции;
  • слабая масштабируемость.

nonempty переносит эту проверку в уровень схемы:

const Schema = nonempty(array(string()))

Производительность и внутренняя логика

nonempty не копирует данные и не трансформирует их. Он выполняет только проверку:

  • вычисление длины строки;
  • проверка length массива.

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


Типизация в TypeScript

При использовании TypeScript nonempty сохраняет тип исходной структуры, но с уточнённым семантическим смыслом.

import { Infer, nonempty, string } from 'superstruct'

const Name = nonempty(string())

type Name = Infer<typeof Name>

Тип остаётся string, однако логическая гарантия «непустоты» не выражается напрямую в типовой системе TypeScript. Это важное ограничение: проверка остаётся runtime-уровнем.


Частые ошибки при использовании

1. Ожидание фильтрации пробелов

nonempty(string())

Не удаляет пробелы. Значение " " считается валидным.


2. Использование с default []

defaulted(nonempty(array(string())), [])

Логически противоречиво: дефолт нарушает nonempty.


3. Ожидание глубокой проверки массива

nonempty(array(string()))

Проверяет только длину массива, а не содержимое строк.


Практические сценарии применения

API входные данные

const CreateOrder = object({
  items: nonempty(array(string())),
  customerId: nonempty(string())
})

Гарантируется, что заказ не создаётся без товаров.


Формы интерфейса

const FormState = object({
  errors: optional(array(string())),
  warnings: optional(array(string()))
})

При добавлении nonempty:

errors: nonempty(array(string()))

форма не допускает пустого списка ошибок как валидного состояния.


Конфигурации

const Plugins = nonempty(array(string()))

Обеспечивает наличие хотя бы одного активного плагина.


Итоговая роль Nonempty в архитектуре схем

nonempty выполняет функцию усилителя ограничений базовых структур. Он переводит проверку из категории «тип корректен» в категорию «данные осмысленны».

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