Nullable и optional значения

При работе с формами и API данные далеко не всегда приходят в полном и корректном виде. Одни поля могут отсутствовать полностью, другие — содержать null, третьи — пустые строки. Библиотека Yup предоставляет гибкий механизм управления такими ситуациями через nullable- и optional-схемы.

Понимание различий между:

  • undefined
  • null
  • отсутствующим полем
  • пустой строкой ""

имеет критическое значение для корректной валидации.


Разница между undefined, null и отсутствующим полем

undefined

Значение существует, но не определено:

const data = {
  age: undefined
}

null

Явное отсутствие значения:

const data = {
  age: null
}

Отсутствующее поле

Свойства вообще нет:

const data = {}

Поведение Yup по умолчанию

По умолчанию большинство схем Yup:

  • разрешают undefined
  • запрещают null

Пример:

import * as yup from 'yup'

const schema = yup.string()

await schema.isValid(undefined) // true
await schema.isValid(null)      // false

Метод nullable()

Метод nullable() разрешает значение null.

Базовый пример

const schema = yup.string().nullable()

await schema.isValid(null) // true

Без nullable():

const schema = yup.string()

await schema.isValid(null) // false

Что меняет nullable()

Метод влияет только на null.

Значение До nullable После nullable
"text" valid valid
undefined valid valid
null invalid valid

Nullable для разных типов

String

const schema = yup.string().nullable()

Number

const schema = yup.number().nullable()

Boolean

const schema = yup.boolean().nullable()

Date

const schema = yup.date().nullable()

Array

const schema = yup.array().nullable()

Object

const schema = yup.object().nullable()

Nullable в формах

Очень частая ситуация — пустое значение select-компонента.

Например:

{
  country: null
}

Без nullable() Yup выдаст ошибку:

const schema = yup.object({
  country: yup.string()
})

Ошибка:

country must be a `string` type

Правильный вариант:

const schema = yup.object({
  country: yup.string().nullable()
})

Метод optional()

Метод optional() делает поле необязательным.

const schema = yup.string().optional()

Теперь значение может отсутствовать:

await schema.isValid(undefined) // true

Важная особенность optional()

Для большинства схем Yup поле уже является optional по умолчанию.

То есть:

yup.string()

и

yup.string().optional()

в большинстве случаев эквивалентны.


Когда optional() действительно полезен

Главная задача optional() — отмена required().

Пример:

const schema = yup.string().required().optional()

Результат:

await schema.isValid(undefined) // true

Метод defined()

Противоположность optional — defined().

Поле обязано существовать и не может быть undefined.

const schema = yup.string().defined()

Проверка:

await schema.isValid(undefined) // false

Разница между defined() и required()

defined()

Запрещает только undefined.

const schema = yup.string().defined()

Допустимо:

''
null // если nullable()

required()

Более строгая проверка.

Для string-схем:

  • запрещает undefined
  • запрещает null
  • запрещает пустую строку
const schema = yup.string().required()

Таблица различий

Метод undefined null “”
string() valid invalid valid
defined() invalid invalid valid
nullable() valid valid valid
required() invalid invalid invalid

Комбинация nullable() и required()

Очень важный нюанс.

Пример

const schema = yup.string().nullable().required()

Результат:

await schema.isValid(null) // false

Почему?

Потому что required() переопределяет nullable-поведение.


Nullable + notRequired

Частая комбинация:

const schema = yup.string().nullable().notRequired()

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

undefined
null
'text'
''

Метод notRequired()

Альтернатива optional().

const schema = yup.string().notRequired()

Фактически:

optional()

и

notRequired()

обычно работают одинаково.


Nullable для number-схем

Проблема пустых input

HTML input часто отправляет:

''

Но number-схема ожидает число.

Пример ошибки:

const schema = yup.number()

await schema.isValid('') // false

Nullable number с transform

Типичное решение:

const schema = yup
  .number()
  .nullable()
  .transform((value, originalValue) => {
    return originalValue === '' ? null : value
  })

Теперь:

await schema.isValid('') // true

Почему transform важен

Yup не преобразует пустую строку в null автоматически.

Без transform:

''

не станет:

null

Nullable date

Дата — ещё один распространённый кейс.

const schema = yup.date().nullable()

Для поддержки пустого input:

const schema = yup
  .date()
  .nullable()
  .transform((curr, orig) => {
    return orig === '' ? null : curr
  })

Optional поля внутри объекта

Пример

const schema = yup.object({
  name: yup.string().required(),
  middleName: yup.string().optional()
})

Допустимо:

{
  name: 'Alex'
}

Nullable поля внутри объекта

const schema = yup.object({
  name: yup.string().required(),
  middleName: yup.string().nullable()
})

Допустимо:

{
  name: 'Alex',
  middleName: null
}

Optional vs nullable

Это разные концепции.

Optional

Поле можно не передавать вообще.

{}

Nullable

Поле должно существовать, но может быть null.

{
  field: null
}

Одновременное использование

const schema = yup.object({
  field: yup.string().nullable().optional()
})

Допустимы:

{}
{
  field: undefined
}
{
  field: null
}
{
  field: 'text'
}

Метод default()

Nullable-логика тесно связана с default-значениями.

Пример

const schema = yup.string().default('Unknown')
schema.cast(undefined)

Результат:

'Unknown'

Как работает default с null

Важно:

schema.cast(null)

не использует default.

default() применяется только к undefined.


Nullable + default

const schema = yup
  .string()
  .nullable()
  .default('Anonymous')

Результаты:

Значение Результат
undefined "Anonymous"
null null

Nullable массивы

Nullable сам массив

const schema = yup.array().nullable()

Допустимо:

null

Nullable элементы массива

const schema = yup.array(
  yup.string().nullable()
)

Допустимо:

['a', null, 'b']

Nullable object

const schema = yup.object({
  name: yup.string()
}).nullable()

Допустимо:

null

Strict mode и nullable

В strict-режиме Yup отключает автоматические преобразования.

const schema = yup
  .number()
  .strict()
  .nullable()

Теперь строка:

'42'

не преобразуется в число.


Nullable и TypeScript

Yup умеет корректно выводить nullable-типы.

Пример

const schema = yup.string().nullable()

Тип:

string | null | undefined

InferType

type User = yup.InferType<typeof schema>

Пример сложной схемы

const userSchema = yup.object({
  id: yup.number().required(),

  nickname: yup
    .string()
    .nullable()
    .optional(),

  age: yup
    .number()
    .nullable()
    .transform((v, o) => {
      return o === '' ? null : v
    }),

  birthDate: yup
    .date()
    .nullable(),

  contacts: yup
    .array(
      yup.string().nullable()
    )
    .optional()
})

Поведение cast()

cast() преобразует данные без полноценной валидации.

Пример

const schema = yup.number().nullable()

schema.cast('42')

Результат:

42

Cast и null

schema.cast(null)

Результат:

null

если используется nullable().


Nullable и oneOf

const schema = yup
  .string()
  .nullable()
  .oneOf(['admin', 'user', null])

Допустимо:

null

Nullable и custom test

const schema = yup
  .string()
  .nullable()
  .test(
    'custom',
    'Ошибка',
    value => {
      if (value === null) {
        return true
      }

      return value.length > 3
    }
  )

Частые ошибки

Ошибка №1: ожидание, что nullable делает поле optional

Неверно:

yup.string().nullable()

Поле всё ещё может участвовать в required-проверках объекта.


Ошибка №2: путаница между null и пустой строкой

''

это не:

null

Ошибка №3: использование required после nullable

yup.string().nullable().required()

null всё равно будет запрещён.


Практический паттерн для форм

Наиболее распространённая схема для необязательных полей формы:

yup
  .string()
  .nullable()
  .transform(value => {
    return value === '' ? null : value
  })

Паттерн для числовых полей

yup
  .number()
  .nullable()
  .transform((value, originalValue) => {
    return originalValue === ''
      ? null
      : value
  })

Паттерн для дат

yup
  .date()
  .nullable()
  .transform((value, originalValue) => {
    return originalValue === ''
      ? null
      : value
  })

Сводная таблица

Комбинация undefined null “”
string() valid invalid valid
string().nullable() valid valid valid
string().required() invalid invalid invalid
string().nullable().optional() valid valid valid
number().nullable() valid valid invalid
number().nullable()+transform valid valid valid
date().nullable() valid valid invalid
date().nullable()+transform valid valid valid