Литеральные типы и их применение

Литеральные типы в Zod представляют собой способ описания строго фиксированных значений, которые допустимы в данных. В отличие от примитивных схем вроде z.string() или z.number(), литералы задают конкретное значение, которое должно совпасть полностью.

Основной примитив для работы с такими значениями — z.literal(). Он позволяет ограничить допустимое значение одним конкретным вариантом.

import { z } from "zod";

const schema = z.literal("success");

schema.parse("success"); // OK
schema.parse("error");   // ошибка

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


Поведение литеральных схем при валидации

Литеральная схема выполняет строгое сравнение значения. Используется эквивалентность ===, поэтому тип и значение должны совпадать полностью.

z.literal(42).parse(42);      // OK
z.literal(42).parse("42");    // ошибка

Для объектов и массивов литералы применяются редко, поскольку сравнение происходит по ссылке, а не по структуре:

const obj = { a: 1 };

z.literal(obj).parse(obj); // OK
z.literal({ a: 1 }).parse({ a: 1 }); // ошибка

Такое поведение делает литералы наиболее полезными для примитивов: строк, чисел, булевых значений.


Булевы и числовые литералы

Литералы часто используются для фиксированных флагов или ограниченных числовых значений.

const activeSchema = z.literal(true);

activeSchema.parse(true);  // OK
activeSchema.parse(false); // ошибка

Числовые литералы применяются в ситуациях, где значение играет роль кода состояния:

const statusCode = z.literal(200);

statusCode.parse(200); // OK
statusCode.parse(404); // ошибка

Объединение литералов через union

Наиболее распространённый сценарий — объединение нескольких литеральных значений. Для этого используется z.union().

const roleSchema = z.union([
  z.literal("admin"),
  z.literal("user"),
  z.literal("guest")
]);

roleSchema.parse("admin"); // OK
roleSchema.parse("root");  // ошибка

Такой подход формирует ограниченный набор допустимых значений без использования enum.


z.enum и отличие от литералов

Zod предоставляет отдельную абстракцию — z.enum(), которая по сути является синтаксическим упрощением для набора строковых литералов.

const roleSchema = z.enum(["admin", "user", "guest"]);

roleSchema.parse("admin"); // OK
roleSchema.parse("root");  // ошибка

Ключевые отличия между z.enum() и z.union(z.literal()):

  • z.enum() работает только со строками
  • z.union(z.literal()) поддерживает любые типы (числа, boolean, mixed)
  • z.enum() генерирует более удобные типы TypeScript
  • union обеспечивает большую гибкость

Использование литералов как дискриминаторов

Литеральные значения часто применяются в дискриминированных объединениях. Это один из ключевых паттернов для типизации структурированных данных.

const eventSchema = z.discriminatedUnion("type", [
  z.object({
    type: z.literal("click"),
    x: z.number(),
    y: z.number()
  }),
  z.object({
    type: z.literal("scroll"),
    scrollTop: z.number()
  })
]);

Поле type выступает дискриминатором, позволяющим автоматически сужать тип.


Сужение типов и вывод TypeScript

Zod тесно интегрируется с TypeScript, и литеральные типы играют важную роль в выводе точных типов.

const schema = z.literal("active");

type Status = z.infer<typeof schema>;
// Status = "active"

При объединении литералов формируется union-тип:

const schema = z.union([
  z.literal("a"),
  z.literal("b"),
  z.literal("c")
]);

type Letters = z.infer<typeof schema>;
// "a" | "b" | "c"

Это позволяет строить строгие контрактные типы без ручного описания TypeScript-интерфейсов.


Литералы в конфигурационных схемах

Одно из практических применений — описание конфигураций, где допустимые значения ограничены фиксированными вариантами.

const configSchema = z.object({
  environment: z.union([
    z.literal("development"),
    z.literal("staging"),
    z.literal("production")
  ])
});

Такой подход исключает возможность передачи произвольных строк и делает контракт явным.


Применение в API-контрактах

Литеральные типы часто используются для описания протоколов взаимодействия между клиентом и сервером.

const requestSchema = z.object({
  method: z.union([
    z.literal("GET"),
    z.literal("POST"),
    z.literal("PUT"),
    z.literal("DELETE")
  ]),
  path: z.string()
});

Это гарантирует, что метод запроса всегда соответствует допустимому HTTP-набору.


Комбинирование с объектными схемами

Литеральные значения часто выступают частью более сложных структур.

const responseSchema = z.object({
  status: z.literal("ok"),
  data: z.object({
    id: z.number(),
    name: z.string()
  })
});

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


Литеральные типы и трансформации

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

const schema = z.literal("double").transform(() => 2);

В этом случае входное значение служит маркером для преобразования.


Расширение через объединения и пересечения

Литералы могут комбинироваться с другими схемами через z.intersection() или вложенные объекты, создавая более сложные контракты.

const base = z.object({
  type: z.literal("event")
});

const clickEvent = z.intersection(
  base,
  z.object({
    x: z.number(),
    y: z.number()
  })
);

Такие конструкции часто используются для построения строгих и расширяемых API-моделей.


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

Одной из распространённых проблем является попытка использовать литералы для структурных объектов:

z.literal({ a: 1 }); // почти всегда не работает как ожидается

Также ошибка возникает при ожидании нестрогого сравнения:

z.literal(1).parse("1"); // ошибка, несмотря на "похожесть"

Литеральные типы не предназначены для приведения типов, только для строгой проверки совпадения.


Поведение при частичной несовместимости типов

Литеральные схемы не выполняют нормализацию или приведение. Любое отклонение от ожидаемого значения приводит к ошибке валидации.

z.literal(true).safeParse(1);
// success: false

Это делает их предсказуемыми и удобными для контрактной валидации.


Роль литералов в архитектуре схем

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

  • идентификаторы состояния
  • типы событий
  • версии протоколов
  • флаги конфигурации
  • дискриминаторы объединений

Их сила заключается не в гибкости, а в жёсткой фиксации допустимых значений, что позволяет строить строгие и самодокументируемые схемы данных.