Объединение типов (union) позволяет описывать значения,
которые могут соответствовать нескольким схемам одновременно. Такой
механизм особенно полезен при работе:
В основе лежит идея: значение считается валидным, если оно прошло проверку хотя бы одной схемы.
import { z } from "zod";
const schema = z.union([
z.string(),
z.number(),
]);
schema.parse("hello"); // OK
schema.parse(42); // OK
schema.parse(true); // Ошибка
Аналогичная сокращённая запись:
const schema = z.string().or(z.number());
z.union() принимает массив схем:
z.union([schema1, schema2, schema3]);
Во время валидации Zod последовательно проверяет каждую схему.
Если одна из схем подошла — проверка завершается успешно.
Если не подошла ни одна — выбрасывается ошибка.
const valueSchema = z.union([
z.string(),
z.boolean(),
z.number(),
]);
valueSchema.parse(false);
valueSchema.parse("text");
valueSchema.parse(100);
Наиболее распространённый сценарий — объединение примитивных типов.
const idSchema = z.union([
z.string(),
z.number(),
]);
Полезно при работе с идентификаторами:
idSchema.parse("123");
idSchema.parse(123);
const flagSchema = z.union([
z.boolean(),
z.null(),
]);
const roleSchema = z.union([
z.literal("admin"),
z.literal("user"),
z.literal("guest"),
]);
Проверка:
roleSchema.parse("admin"); // OK
roleSchema.parse("root"); // Ошибка
Иногда вместо объединения литералов лучше использовать
z.enum().
const statusSchema = z.union([
z.literal("success"),
z.literal("error"),
z.literal("loading"),
]);
const statusSchema = z.enum([
"success",
"error",
"loading",
]);
z.enum():
Одно из важнейших применений — объединение объектных схем.
const catSchema = z.object({
type: z.literal("cat"),
meow: z.boolean(),
});
const dogSchema = z.object({
type: z.literal("dog"),
bark: z.boolean(),
});
const petSchema = z.union([
catSchema,
dogSchema,
]);
Проверка:
petSchema.parse({
type: "cat",
meow: true,
});
petSchema.parse({
type: "dog",
bark: false,
});
Ошибка:
petSchema.parse({
type: "cat",
bark: true,
});
Zod автоматически формирует объединённый TypeScript-тип.
type Pet = z.infer<typeof petSchema>;
Результат:
type Pet =
| {
type: "cat";
meow: boolean;
}
| {
type: "dog";
bark: boolean;
};
TypeScript умеет автоматически сужать типы.
function handlePet(pet: Pet) {
if (pet.type === "cat") {
console.log(pet.meow);
}
if (pet.type === "dog") {
console.log(pet.bark);
}
}
Это один из главных плюсов использования discriminated union.
При большом количестве схем z.union() может работать
медленнее, поскольку Zod проверяет схемы последовательно.
z.union([
schema1,
schema2,
schema3,
schema4,
]);
Каждая схема валидируется по очереди.
z.discriminatedUnion() использует специальное
поле-дискриминатор.
const resultSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("success"),
data: z.string(),
}),
z.object({
type: z.literal("error"),
message: z.string(),
}),
]);
Теперь Zod:
type;const paymentSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal("card"),
cardNumber: z.string(),
}),
z.object({
method: z.literal("paypal"),
email: z.string().email(),
}),
z.object({
method: z.literal("crypto"),
wallet: z.string(),
}),
]);
Использование:
paymentSchema.parse({
method: "paypal",
email: "user@example.com",
});
Вместо последовательной проверки всех схем используется прямой выбор.
Ошибки становятся точнее.
Обычный union:
Invalid input
Discriminated union:
Required field "email"
TypeScript лучше сужает типы.
type Payment = z.infer<typeof paymentSchema>;
function process(payment: Payment) {
switch (payment.method) {
case "card":
payment.cardNumber;
break;
case "paypal":
payment.email;
break;
case "crypto":
payment.wallet;
break;
}
}
Иногда разработчики вручную создают объединение с
undefined.
const schema = z.union([
z.string(),
z.undefined(),
]);
Но правильнее использовать:
const schema = z.string().optional();
Аналогично для null.
const schema = z.union([
z.string(),
z.null(),
]);
const schema = z.string().nullable();
const schema = z.string().nullable().optional();
Тип:
string | null | undefined
const schema = z.union([
z.array(z.string()),
z.array(z.number()),
]);
Допустимые значения:
["a", "b", "c"]
[1, 2, 3]
Недопустимое значение:
["a", 1]
const schema = z.array(
z.union([
z.string(),
z.number(),
])
);
Теперь допустимо:
["a", 1, "b", 2]
Union можно вкладывать друг в друга.
const schema = z.union([
z.string(),
z.union([
z.number(),
z.boolean(),
]),
]);
Но такой код ухудшает читаемость.
Лучше:
const schema = z.union([
z.string(),
z.number(),
z.boolean(),
]);
Объединения хорошо работают с трансформациями.
const schema = z.union([
z.string(),
z.number(),
]).transform((value) => String(value));
Теперь:
schema.parse(100); // "100"
schema.parse("abc"); // "abc"
const schema = z.union([
z.string().transform((v) => v.toUpperCase()),
z.number().transform((v) => v * 2),
]);
Результат:
schema.parse("hello"); // "HELLO"
schema.parse(5); // 10
Дополнительные проверки можно применять ко всему объединению.
const schema = z.union([
z.string(),
z.number(),
]).refine((value) => {
return String(value).length >= 3;
});
const schema = z.union([
z.string(),
z.number(),
]).refine((value) => {
if (typeof value === "string") {
return value.startsWith("A");
}
return value > 100;
});
Предварительная обработка часто используется вместе с union.
const schema = z.preprocess(
(value) => {
if (typeof value === "string" && !isNaN(Number(value))) {
return Number(value);
}
return value;
},
z.union([
z.string(),
z.number(),
])
);
Ошибки union бывают объёмными, потому что Zod хранит информацию обо всех неудачных проверках.
const schema = z.union([
z.string(),
z.number(),
]);
const result = schema.safeParse(true);
console.log(result.error);
parse() выбрасывает исключение.
schema.parse(value);
safeParse() возвращает объект результата.
const result = schema.safeParse(value);
if (!result.success) {
console.log(result.error.format());
}
const schema = z.union([
z.object({
type: z.literal("a"),
value: z.string(),
}),
z.object({
type: z.literal("b"),
count: z.number(),
}),
]);
Ошибка:
schema.safeParse({
type: "a",
count: 5,
});
Zod покажет:
z.union([
schema1,
schema2,
schema3,
schema4,
]);
Каждая схема проверяется по очереди.
z.discriminatedUnion("type", [...]);
Используется прямой выбор ветки.
При большом количестве вариантов discriminatedUnion
значительно эффективнее.
z.union([
z.object({
value: z.string(),
}),
z.object({
value: z.string(),
extra: z.boolean(),
}),
]);
Такие схемы могут вызывать неоднозначность.
z.union([
schema1,
schema2,
schema3,
schema4,
schema5,
schema6,
schema7,
]);
При большом количестве вариантов:
const apiResponseSchema = z.discriminatedUnion("status", [
z.object({
status: z.literal("success"),
data: z.object({
users: z.array(z.string()),
}),
}),
z.object({
status: z.literal("error"),
error: z.string(),
}),
z.object({
status: z.literal("loading"),
}),
]);
Использование:
const response = apiResponseSchema.parse(data);
switch (response.status) {
case "success":
console.log(response.data.users);
break;
case "error":
console.log(response.error);
break;
case "loading":
console.log("Loading...");
break;
}
const authSchema = z.discriminatedUnion("provider", [
z.object({
provider: z.literal("email"),
email: z.string().email(),
password: z.string(),
}),
z.object({
provider: z.literal("google"),
token: z.string(),
}),
z.object({
provider: z.literal("github"),
accessToken: z.string(),
}),
]);
const eventSchema = z.discriminatedUnion("event", [
z.object({
event: z.literal("click"),
x: z.number(),
y: z.number(),
}),
z.object({
event: z.literal("scroll"),
position: z.number(),
}),
z.object({
event: z.literal("keydown"),
key: z.string(),
}),
]);
Объединяет альтернативы.
A | B
z.union([A, B])
Значение должно соответствовать хотя бы одной схеме.
Объединяет требования.
A & B
z.intersection(A, B)
Значение обязано соответствовать обеим схемам одновременно.
z.union([
z.string(),
z.number(),
]);
Строго ограничивает набор типов.
z.any()
Разрешает абсолютно всё.
z.unknown()
Разрешает любое значение, но требует дальнейшей проверки.
z.union([
z.string(),
z.number(),
]);
Сразу задаёт допустимые варианты.
Предпочтительно:
z.discriminatedUnion("type", [...]);
При большом количестве веток лучше пересмотреть архитектуру схем.
Хороший вариант:
type: z.literal("success")
Плохой вариант:
type: z.string()
Слишком глубокие union сложно поддерживать.
z.enum(["a", "b", "c"])
обычно лучше, чем:
z.union([
z.literal("a"),
z.literal("b"),
z.literal("c"),
]);