Пересечения типов (intersection)

Пересечение типов (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);

Что происходит на уровне TypeScript

Пересечение создаёт тип:

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);

Теперь строка обязана:

  • быть email;
  • иметь минимум 5 символов.

Пример проверки

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

Пересечение union-схем

Комбинирование union

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");

Ошибка.


Пересечение с refine

Комбинация независимых правил

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",
});

Пересечение и optional-поля

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

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
}

Конфликт optional и required

const A = z.object({
  value: z.string(),
});

const B = z.object({
  value: z.string().optional(),
});

Пересечение:

const Result = A.and(B);

Итоговое поле будет обязательным.

Причина:

  • первая схема требует поле;
  • пересечение требует выполнения обеих схем.

Пересечение и nullable

const A = z.string();

const B = z.string().nullable();

const Result = A.and(B);

Итог:

string

Почему:

  • первая схема запрещает null;
  • пересечение оставляет только допустимые для обеих схем значения.

Пересечение discriminated union

Проблемный сценарий

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-схем

Особенность transform

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

Фактически — невозможный тип.


Ошибки в intersection

Формат ошибок

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({});

Ошибки будут собраны из обеих схем.


Производительность intersection

Двойная валидация

Важно учитывать:

z.intersection(A, B)

проверяет:

  • сначала A;
  • затем B.

Это означает двойной проход по данным.


Влияние на большие схемы

На крупных объектах с:

  • refine;
  • transform;
  • сложными union;
  • вложенными структурами;

пересечения могут быть заметно медленнее 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);

Ограничения intersection

Невозможные пересечения

const A = z.string();

const B = z.number();

const Result = A.and(B);

Такое значение невозможно создать.

Проверка всегда завершится ошибкой.


Конфликтующие literal

const A = z.literal("admin");

const B = z.literal("user");

const Result = A.and(B);

Итог:

never

Проверка через safeParse

Без выбрасывания исключений

const Result =
  z.string()
    .min(5)
    .and(z.string().email());

const parsed =
  Result.safeParse("abc");

Проверка:

if (!parsed.success) {
  console.log(parsed.error.format());
}

Комбинирование intersection и union

Сложные схемы

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()
}

Архитектурные рекомендации

Когда intersection подходит лучше всего

Пересечения особенно полезны для:

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

Когда intersection лучше избегать

Нежелательно использовать пересечения:

  • при конфликтующих трансформациях;
  • при пересечении несовместимых типов;
  • в горячих участках производительности;
  • вместо merge() для простого объединения объектов.

Сравнение intersection и union

union

z.union([A, B])

Значение должно соответствовать:

A ИЛИ B

intersection

z.intersection(A, B)

Значение должно соответствовать:

A И B

Внутреннее поведение intersection

Во время проверки Zod:

  1. валидирует данные через первую схему;
  2. валидирует данные через вторую схему;
  3. объединяет результаты;
  4. собирает ошибки обеих схем;
  5. формирует итоговый тип пересечения.

Из-за этого intersection значительно строже обычного объединения shape-объектов.


Краткая сводка

intersection

z.intersection(A, B)

или:

A.and(B)

Основные свойства

  • обе схемы должны пройти проверку;
  • работает не только с объектами;
  • создаёт тип A & B;
  • полезен для композиции правил;
  • может приводить к невозможным типам;
  • выполняет двойную валидацию;
  • отличается от merge() принципом работы.

Наиболее частые сценарии использования

Композиция сущностей

User.and(WithId).and(WithDates)

Усиление ограничений

Email.and(MinLength)

Повторное использование бизнес-правил

Positive.and(Integer)

Комбинирование модулей схем

Auth.and(Metadata)