Валидация данных в JavaScript часто сводится не только к проверке типа значения, но и к проверке его содержимого. Одной из типичных проблем являются «пустые, но формально корректные» значения: пустые строки, пустые массивы, коллекции без элементов. Они проходят базовые проверки типов, но в реальной бизнес-логике почти всегда считаются ошибочными.
В библиотеке Superstruct подобные ограничения реализуются через
композицию структур и специализированные утилиты. Одной из ключевых
является nonempty, обеспечивающая инвариант: значение
существует и содержит хотя бы один элемент или символ.
nonempty работает как обёртка над существующей
структурой и добавляет дополнительное ограничение:
Общая идея заключается в том, что значение должно быть не просто валидного типа, но и не «пустым контейнером».
В 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 ""
Ошибки формируются на уровне структуры-обёртки, что позволяет точно локализовать проблему.
nonempty может конфликтовать с концепцией необязательных
значений, поэтому важно различать:
undefined)"", [])import { optional } from 'superstruct'
const Name = optional(nonempty(string()))
Такое определение означает:
import { defaulted } from 'superstruct'
const Tags = defaulted(nonempty(array(string())), [])
Здесь возникает логическое противоречие: default []
конфликтует с nonempty. В результате такая конструкция
часто используется с осторожностью, либо заменяется на более
осмысленную:
const Tags = nonempty(array(string()))
без дефолта, с явной обязательностью данных.
При работе со сложными объектами важно понимать границы применения.
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 nonempty сохраняет тип
исходной структуры, но с уточнённым семантическим смыслом.
import { Infer, nonempty, string } from 'superstruct'
const Name = nonempty(string())
type Name = Infer<typeof Name>
Тип остаётся string, однако логическая гарантия
«непустоты» не выражается напрямую в типовой системе TypeScript. Это
важное ограничение: проверка остаётся runtime-уровнем.
nonempty(string())
Не удаляет пробелы. Значение " " считается
валидным.
defaulted(nonempty(array(string())), [])
Логически противоречиво: дефолт нарушает nonempty.
nonempty(array(string()))
Проверяет только длину массива, а не содержимое строк.
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 выполняет функцию усилителя ограничений базовых
структур. Он переводит проверку из категории «тип корректен» в категорию
«данные осмысленны».
В композиции с другими валидаторами Superstruct он становится инструментом формирования строгих контрактов данных, где пустые состояния исключаются на уровне схемы, а не бизнес-логики.