Опциональные и nullable поля

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

В Zod опциональность задаётся методом .optional().

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

import { z } from "zod";

const UserSchema = z.object({
  name: z.string(),
  age: z.number().optional(),
});

Поле age:

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

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

UserSchema.parse({
  name: "Alex",
});

UserSchema.parse({
  name: "Alex",
  age: 25,
});

Ошибка:

UserSchema.parse({
  name: "Alex",
  age: "25",
});

Результат:

Expected number, received string

Как работает .optional()

Метод .optional() превращает схему:

z.string()

в:

z.string().optional()

что эквивалентно:

z.union([z.string(), z.undefined()])

То есть схема начинает принимать:

  • строку;
  • undefined.

Типы TypeScript

Zod автоматически выводит корректный тип.

const Schema = z.object({
  title: z.string().optional(),
});

type SchemaType = z.infer<typeof Schema>;

Получится:

type SchemaType = {
  title?: string | undefined;
};

Важно понимать:

title?: string

и

title: string | undefined

— не одно и то же.

Отличие

Опциональное свойство

{
  title?: string;
}

Свойство может отсутствовать:

{}

Свойство со значением undefined

{
  title: string | undefined;
}

Свойство обязательно должно существовать:

{
  title: undefined
}

Zod учитывает эту разницу.


Nullable поля

Nullable-поле — это поле, которое может содержать null.

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

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

const UserSchema = z.object({
  avatar: z.string().nullable(),
});

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

UserSchema.parse({
  avatar: "avatar.png",
});

UserSchema.parse({
  avatar: null,
});

Ошибка:

UserSchema.parse({
  avatar: undefined,
});

Как работает .nullable()

z.string().nullable()

эквивалентно:

z.union([z.string(), z.null()])

Схема принимает:

  • строку;
  • null.

Типы TypeScript для nullable

const Schema = z.object({
  avatar: z.string().nullable(),
});

type SchemaType = z.infer<typeof Schema>;

Результат:

type SchemaType = {
  avatar: string | null;
};

Поле обязательно присутствует, но может быть null.


Разница между optional и nullable

Это одна из важнейших тем при работе с Zod.

.optional()

z.string().optional()

Разрешает:

undefined

или отсутствие поля.

Пример

const Schema = z.object({
  name: z.string().optional(),
});

Допустимо:

{}

Допустимо:

{
  name: undefined
}

.nullable()

z.string().nullable()

Разрешает:

null

Но поле обязано существовать.

Пример

const Schema = z.object({
  name: z.string().nullable(),
});

Допустимо:

{
  name: null
}

Ошибка:

{}

Optional + Nullable одновременно

Очень распространённая ситуация — поле может:

  • отсутствовать;
  • быть null;
  • содержать значение.

Для этого используется комбинация:

z.string().optional().nullable()

или:

z.string().nullable().optional()

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


Пример

const Schema = z.object({
  description: z.string().optional().nullable(),
});

Допустимо:

{}
{
  description: undefined
}
{
  description: null
}
{
  description: "Text"
}

Итоговый TypeScript-тип

type SchemaType = {
  description?: string | null | undefined;
};

Метод .nullish()

В Zod существует сокращение:

.nullish()

Оно объединяет:

.optional().nullable()

Пример

const Schema = z.object({
  bio: z.string().nullish(),
});

Эквивалент:

const Schema = z.object({
  bio: z.string().optional().nullable(),
});

Что принимает .nullish()

{}
{
  bio: undefined
}
{
  bio: null
}
{
  bio: "Developer"
}

Optional поля в объектах

Частично заполненные объекты

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

Для этого используется .partial().


Пример

const UserSchema = z.object({
  name: z.string(),
  email: z.string(),
  age: z.number(),
});

const PartialUserSchema = UserSchema.partial();

Теперь все поля опциональны.

Допустимо:

{}
{
  name: "Alex"
}
{
  email: "test@test.com"
}

Итоговый тип

type PartialUser = {
  name?: string;
  email?: string;
  age?: number;
};

Частичная опциональность

.partial() может принимать объект с настройками.

Пример

const UserSchema = z.object({
  name: z.string(),
  email: z.string(),
  age: z.number(),
});

const UpdateSchema = UserSchema.partial({
  age: true,
});

Только age станет опциональным.


Глубокая опциональность

Для вложенных объектов используется .deepPartial().

Пример

const Schema = z.object({
  user: z.object({
    profile: z.object({
      avatar: z.string(),
    }),
  }),
});

const DeepSchema = Schema.deepPartial();

Теперь опциональными станут все вложенные поля.

Допустимо:

{}
{
  user: {}
}
{
  user: {
    profile: {}
  }
}

Nullable в массивах

Nullable-массив

const Schema = z.array(z.string()).nullable();

Допустимо:

null
["a", "b"]

Ошибка:

[1, 2]

Массив nullable-значений

const Schema = z.array(
  z.string().nullable()
);

Допустимо:

["a", null, "b"]

Комбинация

const Schema = z.array(
  z.string().nullable()
).optional();

Допустимо:

undefined
null

для элементов массива:

["a", null]

Optional и default

Метод .default()

default() автоматически подставляет значение, если поле отсутствует или равно undefined.


Пример

const Schema = z.object({
  role: z.string().default("user"),
});
Schema.parse({});

Результат:

{
  role: "user"
}

Важная особенность

.default() автоматически делает поле optional.

То есть:

z.string().default("test")

уже не требует .optional().


Поведение с null

const Schema = z.object({
  role: z.string().default("user"),
});
Schema.parse({
  role: null,
});

Ошибка:

Expected string, received null

Потому что default() работает только с:

  • отсутствующим полем;
  • undefined.

Nullable + default

Пример

const Schema = z.object({
  role: z.string().nullable().default("user"),
});

Допустимо:

{}

Результат:

{
  role: "user"
}

Передача null

Schema.parse({
  role: null,
});

Результат:

{
  role: null
}

null не заменяется значением по умолчанию.


Optional в refine

При использовании .refine() важно учитывать возможное отсутствие значения.

Ошибочный пример

const Schema = z.string()
  .optional()
  .refine((value) => value.length > 3);

Проблема:

value

может быть undefined.


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

const Schema = z.string()
  .optional()
  .refine((value) => {
    if (value === undefined) {
      return true;
    }

    return value.length > 3;
  });

Nullable в transform

Пример

const Schema = z.string()
  .nullable()
  .transform((value) => {
    if (value === null) {
      return "empty";
    }

    return value.toUpperCase();
  });

Optional и preprocess

preprocess позволяет преобразовывать входные данные до валидации.

Пример преобразования null → undefined

const Schema = z.preprocess(
  (value) => {
    if (value === null) {
      return undefined;
    }

    return value;
  },
  z.string().optional()
);

Теперь:

Schema.parse(null);

пройдёт успешно.


Optional поля в API

PATCH-запросы

Для PATCH-запросов поля почти всегда должны быть optional.

Пример

const UpdateUserSchema = z.object({
  name: z.string().optional(),
  email: z.string().email().optional(),
  age: z.number().optional(),
});

Клиент может отправить только изменяемые поля.


Nullable поля в базе данных

Во многих ORM и SQL-базах:

  • NULL означает отсутствие значения;
  • отсутствие поля означает, что поле не было передано.

Это критически важно различать.


Пример

{
  avatar: null
}

означает:

удалить значение

А:

{}

означает:

не изменять поле


Практический пример DTO

Создание пользователя

const CreateUserSchema = z.object({
  email: z.string().email(),
  password: z.string(),
  nickname: z.string().optional(),
  avatar: z.string().nullable(),
});

Обновление пользователя

const UpdateUserSchema = z.object({
  email: z.string().email().optional(),
  password: z.string().optional(),
  nickname: z.string().optional(),
  avatar: z.string().nullable().optional(),
});

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

Путаница между null и undefined

Ошибка

z.string().optional()

не принимает:

null

Ошибка при nullable-поле

z.string().nullable()

не принимает:

undefined

Лишний optional после default

Избыточный код

z.string().optional().default("test")

Достаточно:

z.string().default("test")

Неправильная обработка optional в refine

Ошибка

.refine(value => value.length > 5)

Проблема

value может быть undefined.


Рекомендации по использованию

Использование .optional()

Подходит для:

  • PATCH DTO;
  • необязательных параметров;
  • частичных конфигураций;
  • форм с неполным вводом.

Использование .nullable()

Подходит для:

  • SQL NULL;
  • очищаемых полей;
  • nullable-колонок БД;
  • API с явным отсутствием значения.

Использование .nullish()

Подходит, когда:

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

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

Схема undefined null отсутствие поля
z.string()
z.string().optional()
z.string().nullable()
z.string().nullish()