Пересечение типов (intersection) позволяет объединять
несколько схем в одну таким образом, чтобы итоговое значение
одновременно удовлетворяло всем условиям объединяемых схем.
В Zod пересечения реализуются через:
z.intersection(schemaA, schemaB)
или сокращённую форму:
schemaA.and(schemaB)
Обе записи эквивалентны.
Если имеются две схемы:
import { z } from "zod";
const A = z.object({
name: z.string(),
});
const B = z.object({
age: z.number(),
});
Пересечение создаётся так:
const Person = z.intersection(A, B);
Теперь объект обязан соответствовать обеим схемам одновременно:
Person.parse({
name: "Alex",
age: 25,
});
Результат:
{
name: "Alex",
age: 25
}
Если хотя бы одна часть невалидна — вся проверка завершится ошибкой.
.and()Метод .and() делает код более читаемым:
const Person = A.and(B);
Полный эквивалент:
const Person = z.intersection(A, B);
Пересечение создаёт тип:
type Result = TypeA & TypeB
Пример:
const A = z.object({
title: z.string(),
});
const B = z.object({
views: z.number(),
});
const Article = A.and(B);
type Article = z.infer<typeof Article>;
Итоговый тип:
type Article = {
title: string;
} & {
views: number;
}
После развёртывания:
type Article = {
title: string;
views: number;
}
Наиболее распространённый сценарий — объединение объектов.
Часто схема разбивается на логические части:
const TimestampFields = z.object({
createdAt: z.date(),
updatedAt: z.date(),
});
const UserFields = z.object({
id: z.string(),
email: z.string().email(),
});
Объединение:
const User = TimestampFields.and(UserFields);
Итог:
User.parse({
id: "u1",
email: "admin@mail.com",
createdAt: new Date(),
updatedAt: new Date(),
});
Пересечения особенно полезны при композиции:
const WithId = z.object({
id: z.string(),
});
const WithAudit = z.object({
createdAt: z.date(),
updatedAt: z.date(),
});
const Product = z.object({
title: z.string(),
price: z.number(),
});
const ProductEntity =
Product
.and(WithId)
.and(WithAudit);
merge() и intersection()Эти методы часто путают.
merge()Работает только с z.object():
const Result = A.merge(B);
intersection()Работает с любыми схемами:
const Result = z.intersection(A, B);
merge() объединяет
shape объектовconst A = z.object({
name: z.string(),
});
const B = z.object({
age: z.number(),
});
const Result = A.merge(B);
intersection()
валидирует обе схемы независимоconst Result = z.intersection(A, B);
merge()Правая схема перезаписывает левую:
const A = z.object({
value: z.string(),
});
const B = z.object({
value: z.number(),
});
const Result = A.merge(B);
Итог:
{
value: number
}
intersection()Обе схемы должны быть истинны одновременно.
const Result = z.intersection(A, B);
Теперь поле должно быть одновременно:
string & number
Такого значения не существует.
Проверка всегда будет падать:
Result.parse({
value: 123,
});
Ошибка:
Expected string, received number
const MinLength = z.string().min(5);
const Email = z.string().email();
const EmailWithMinLength =
z.intersection(MinLength, Email);
Теперь строка обязана:
EmailWithMinLength.parse("a@b.c");
Ошибка:
String must contain at least 5 character(s)
const StartsWithAdmin =
z.string().regex(/^admin/);
const EndsWithCom =
z.string().regex(/\.com$/);
const AdminCom =
StartsWithAdmin.and(EndsWithCom);
Валидно:
admin@test.com
Невалидно:
user@test.com
const A = z.union([
z.literal("a"),
z.literal("b"),
]);
const B = z.union([
z.literal("b"),
z.literal("c"),
]);
const Result = z.intersection(A, B);
Итоговая допустимая область:
"b"
Result.parse("b");
Успешно.
Result.parse("a");
Ошибка.
const Positive =
z.number().refine(n => n > 0);
const Integer =
z.number().int();
const PositiveInteger =
Positive.and(Integer);
Допустимо:
10
Недопустимо:
-5
3.14
const PasswordLength =
z.string().min(8);
const PasswordUppercase =
z.string().refine(
val => /[A-Z]/.test(val),
{
message: "Нет заглавной буквы",
}
);
const StrongPassword =
PasswordLength.and(PasswordUppercase);
const Address = z.object({
city: z.string(),
street: z.string(),
});
const Contact = z.object({
phone: z.string(),
});
const User =
z.object({
name: z.string(),
address: Address,
})
.and(Contact);
User.parse({
name: "Ivan",
address: {
city: "Moscow",
street: "Lenina",
},
phone: "+79999999999",
});
const A = z.object({
name: z.string(),
});
const B = z.object({
age: z.number().optional(),
});
const Result = A.and(B);
Допустимо:
{
name: "Alex"
}
И:
{
name: "Alex",
age: 25
}
const A = z.object({
value: z.string(),
});
const B = z.object({
value: z.string().optional(),
});
Пересечение:
const Result = A.and(B);
Итоговое поле будет обязательным.
Причина:
const A = z.string();
const B = z.string().nullable();
const Result = A.and(B);
Итог:
string
Почему:
null;const Cat = z.object({
type: z.literal("cat"),
lives: z.number(),
});
const Dog = z.object({
type: z.literal("dog"),
bark: z.boolean(),
});
const Animal = z.discriminatedUnion("type", [
Cat,
Dog,
]);
Пересечение:
const Timestamped =
z.object({
createdAt: z.date(),
});
const Result =
Animal.and(Timestamped);
Работает корректно:
Result.parse({
type: "cat",
lives: 9,
createdAt: new Date(),
});
const Numbers =
z.array(z.number());
const NonEmpty =
z.array(z.number()).min(1);
const Result =
Numbers.and(NonEmpty);
Теперь массив обязан:
transform() меняет выходное значение.
const A = z.string().transform(val => val.trim());
const B = z.string().min(3);
const Result = A.and(B);
Такие комбинации требуют осторожности.
При пересечении Zod запускает обе схемы отдельно.
Если обе схемы трансформируют данные по-разному, возможны неожиданные результаты.
Пример:
const A =
z.string().transform(v => v.toUpperCase());
const B =
z.string().transform(v => v.length);
const Result = A.and(B);
Здесь возникает конфликт типов результата:
string & number
Фактически — невозможный тип.
const A = z.object({
age: z.number(),
});
const B = z.object({
age: z.number().min(18),
});
const Result = A.and(B);
Ошибка:
Result.safeParse({
age: 10,
});
Результат:
{
success: false,
error: ZodError
}
const A = z.object({
name: z.string(),
});
const B = z.object({
age: z.number(),
});
const Result = A.and(B);
Проверка:
Result.safeParse({});
Ошибки будут собраны из обеих схем.
Важно учитывать:
z.intersection(A, B)
проверяет:
A;B.Это означает двойной проход по данным.
На крупных объектах с:
refine;transform;пересечения могут быть заметно медленнее merge().
merge()Если требуется:
предпочтительнее:
A.merge(B)
const WithId = z.object({
id: z.string(),
});
const WithTimestamps = z.object({
createdAt: z.date(),
updatedAt: z.date(),
});
const SoftDelete = z.object({
deletedAt: z.date().nullable(),
});
const User = z.object({
email: z.string().email(),
password: z.string(),
});
const FullUser =
User
.and(WithId)
.and(WithTimestamps)
.and(SoftDelete);
const A = z.string();
const B = z.number();
const Result = A.and(B);
Такое значение невозможно создать.
Проверка всегда завершится ошибкой.
const A = z.literal("admin");
const B = z.literal("user");
const Result = A.and(B);
Итог:
never
const Result =
z.string()
.min(5)
.and(z.string().email());
const parsed =
Result.safeParse("abc");
Проверка:
if (!parsed.success) {
console.log(parsed.error.format());
}
const Auth =
z.union([
z.object({
type: z.literal("token"),
token: z.string(),
}),
z.object({
type: z.literal("session"),
sessionId: z.string(),
}),
]);
const Metadata =
z.object({
createdAt: z.date(),
});
const Request =
Auth.and(Metadata);
{
type: "token",
token: "abc",
createdAt: new Date()
}
Пересечения особенно полезны для:
Нежелательно использовать пересечения:
merge() для простого объединения объектов.intersection и unionunionz.union([A, B])
Значение должно соответствовать:
A ИЛИ B
intersectionz.intersection(A, B)
Значение должно соответствовать:
A И B
Во время проверки Zod:
Из-за этого intersection значительно строже обычного
объединения shape-объектов.
intersectionz.intersection(A, B)
или:
A.and(B)
A & B;merge() принципом работы.User.and(WithId).and(WithDates)
Email.and(MinLength)
Positive.and(Integer)
Auth.and(Metadata)