Nullable

Назначение Nullable и базовая идея

nullable в Superstruct используется для описания структур, допускающих значение null. Это один из ключевых примитивов, позволяющий явно моделировать отсутствие значения, сохраняя при этом строгую валидацию типов.

В отличие от ситуаций, где null может появляться неявно, nullable делает это поведение частью схемы данных, а не побочным эффектом.

Основная идея:

  • значение может быть заданного типа;
  • либо null;
  • любые другие типы считаются ошибкой.

Синтаксис и базовое использование

Функция nullable оборачивает любую структуру Superstruct и расширяет допустимые значения.

import { string, nullable, assert } from 'superstruct';

const Name = nullable(string());

assert('Alex', Name);   // корректно
assert(null, Name);     // корректно
assert(123, Name);      // ошибка

В этом примере структура Name допускает строку или null, но исключает все остальные типы.


Поведение при валидации

При проверке значений nullable работает как логическая обёртка:

  1. Если значение равно null → проверка проходит.
  2. Если значение не null → выполняется валидация вложенной структуры.
  3. Если вложенная структура не проходит проверку → возвращается ошибка.
import { number, nullable, validate } from 'superstruct';

const Age = nullable(number());

validate(25, Age);   // [undefined, 25]
validate(null, Age); // [undefined, null]
validate('25', Age); // [Error]

Отличие Nullable от Optional

Одно из частых заблуждений связано с различием между nullable и optional.

Nullable

Разрешает null как валидное значение.

Optional

Разрешает отсутствие значения (undefined).

import { optional, nullable, string } from 'superstruct';

const A = nullable(string());
const B = optional(string());

Сравнение поведения:

Значение nullable(string) optional(string)
“text” допустимо допустимо
null допустимо недопустимо
undefined недопустимо допустимо

Комбинация возможна:

import { nullable, optional, string } from 'superstruct';

const C = optional(nullable(string()));

Такой вариант допускает:

  • строку
  • null
  • undefined

Использование в сложных структурах

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

import { object, string, nullable } from 'superstruct';

const User = object({
  name: string(),
  middleName: nullable(string()),
});

Примеры:

validate(
  { name: 'Ivan', middleName: null },
  User
);

validate(
  { name: 'Ivan', middleName: 'Petrovich' },
  User
);

Ошибочные случаи:

validate(
  { name: 'Ivan', middleName: 123 },
  User
);

Nullable и вложенные структуры

nullable корректно работает с любыми типами Superstruct, включая сложные композиции.

import { array, nullable, number } from 'superstruct';

const Scores = nullable(array(number()));

validate([1, 2, 3], Scores); // ок
validate(null, Scores);      // ок
validate([1, '2'], Scores);   // ошибка

Композиция с refine и transform

nullable сохраняет поведение всех обёрнутых структур, включая пользовательские проверки.

import { refine, nullable, string } from 'superstruct';

const NonEmptyString = refine(string(), 'NonEmptyString', (v) => v.length > 0);

const Field = nullable(NonEmptyString);

validate(null, Field);      // ок
validate('hello', Field);   // ок
validate('', Field);        // ошибка

Важно: null проверку refine не проходит, потому что обработка происходит до выполнения внутренних правил.


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

При нарушении правил nullable возвращает ошибку, указывающую на несоответствие вложенной структуры.

Пример:

import { validate, nullable, number } from 'superstruct';

const Age = nullable(number());

const [error] = validate('abc', Age);

console.log(error);

Типичная ошибка будет указывать на ожидание number или null.


Использование в API-валидации

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

import { object, string, nullable } from 'superstruct';

const Profile = object({
  username: string(),
  avatarUrl: nullable(string()),
  bio: nullable(string()),
});

Такая модель позволяет:

  • хранить необязательные поля без их удаления;
  • явно фиксировать отсутствие значения через null;
  • избегать смешения undefined и null в данных.

Nullable в массивах и коллекциях

При работе с массивами nullable может применяться как к элементам, так и к самому массиву.

Nullable элементы

import { array, nullable, string } from 'superstruct';

const Tags = array(nullable(string()));

Допустимые значения:

['js', null, 'ts']

Nullable массив

const Tags = nullable(array(string()));

Допустимые значения:

  • ['js', 'ts']
  • null

Поведение при сериализации данных

nullable не влияет на структуру данных вне валидации. Он не преобразует значения и не модифицирует их.

Это означает:

  • null остаётся null;
  • валидные значения возвращаются без изменений.
const [error, value] = validate('text', nullable(string()));

// value === 'text'

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

Ошибка 1: ожидание поведения как у optional

const Field = nullable(string());

Ожидание: поле может отсутствовать Реальность: отсутствие поля (undefined) не разрешено

Ошибка 2: смешивание undefined и null

const Field = nullable(string());

Передача undefined приводит к ошибке, несмотря на “логическое отсутствие значения”.

Ошибка 3: неправильная композиция с default

import { defaulted, nullable, string } from 'superstruct';

const Field = defaulted(nullable(string()), 'text');

Здесь null не заменяется значением по умолчанию, так как он считается валидным значением.


Nullable в архитектуре типов данных

Использование nullable часто связано с выбором стратегии представления отсутствующих значений:

  • null — явное отсутствие значения;
  • undefined — неопределённое состояние;
  • отсутствие поля — структурная неполнота.

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