Расширение существующих типов

Работа с типами в Zod строится вокруг идеи композиции: базовые схемы можно последовательно модифицировать, расширять и комбинировать, не теряя строгой типизации TypeScript. Расширение существующих типов используется при эволюции доменной модели, когда требуется добавить новые поля, изменить обязательность свойств или объединить несколько схем в одну.


Базовое расширение объекта через .extend()

Основной механизм добавления новых полей в уже существующую объектную схему — метод .extend().

import { z } from "zod";

const UserBase = z.object({
  id: z.string(),
  name: z.string(),
});

const UserWithEmail = UserBase.extend({
  email: z.string().email(),
});

Результирующая схема сохраняет все поля исходного объекта и добавляет новые. Типизация автоматически обновляется:

  • id — string
  • name — string
  • email — string

Ключевая особенность заключается в том, что расширение не мутирует исходную схему. Это позволяет строить цепочки производных моделей без риска побочных эффектов.


Расширение с переопределением полей

Метод .extend() также допускает переопределение типов существующих свойств. Это используется, когда необходимо уточнить или изменить правило в производной схеме.

const Base = z.object({
  id: z.string(),
  role: z.string(),
});

const Admin = Base.extend({
  role: z.literal("admin"),
});

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


Объединение схем через .merge()

Когда требуется объединить две независимые схемы, используется .merge().

const ContactInfo = z.object({
  email: z.string().email(),
  phone: z.string(),
});

const Profile = z.object({
  id: z.string(),
  name: z.string(),
});

const FullProfile = Profile.merge(ContactInfo);

Особенности поведения:

  • обе схемы должны быть объектами
  • конфликтующие ключи должны быть совместимы по типам
  • результатом является единая схема с объединёнными полями

.merge() предпочтителен, когда логически существуют две независимые сущности, которые нужно соединить.


Пересечение типов через z.intersection()

Альтернативой .merge() является явное пересечение типов:

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

const B = z.object({
  b: z.number(),
});

const C = z.intersection(A, B);

Разница между .merge() и z.intersection() заключается в уровне явности и гибкости: пересечение работает с любыми схемами, включая более сложные случаи, тогда как merge ориентирован на объекты.


Частичное расширение через .partial()

Иногда расширение связано не с добавлением полей, а с изменением их обязательности.

const User = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string(),
});

const PartialUser = User.partial();

Все поля становятся необязательными:

{
  id?: string;
  name?: string;
  email?: string;
}

Метод часто используется при моделировании PATCH-запросов или частичных обновлений сущностей.


Точечное изменение обязательности через .pick() и .omit()

Хотя эти методы не добавляют новые поля напрямую, они часто участвуют в расширении модели за счёт создания производных типов.

.pick()

const UserPublic = User.pick({
  id: true,
  name: true,
});

Создаётся новая схема, содержащая только выбранные поля.

.omit()

const UserSafe = User.omit({
  email: true,
});

Используется для удаления чувствительных данных из производной модели.


Расширение через композицию базовых схем

В крупных системах распространён подход создания базовых “кирпичей”, которые затем расширяются в конкретных контекстах.

const Timestamped = z.object({
  createdAt: z.date(),
  updatedAt: z.date(),
});

const PostBase = z.object({
  id: z.string(),
  title: z.string(),
});

const Post = PostBase.extend({
  content: z.string(),
}).merge(Timestamped);

Такой стиль позволяет:

  • повторно использовать общие поля
  • избегать дублирования
  • централизовать изменения

Уточнение типов через .refine() и .superRefine()

Расширение типов может происходить не только на уровне структуры, но и на уровне логики валидации.

const UserId = z.string().refine((val) => val.startsWith("usr_"), {
  message: "Invalid user id",
});

.superRefine() позволяет добавлять сложную логику, влияющую на итоговую модель без изменения её структуры.


Добавление необязательных полей

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

const Base = z.object({
  id: z.string(),
});

const Extended = Base.extend({
  metadata: z.object({
    source: z.string(),
  }).optional(),
});

Это позволяет безопасно эволюционировать API без нарушения обратной совместимости.


Использование catchall как форма расширяемости

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

const Flexible = z.object({
  id: z.string(),
}).catchall(z.any());

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


Динамическое расширение схем

В реальных приложениях расширение часто строится динамически:

function withAudit(schema) {
  return schema.extend({
    createdBy: z.string(),
    updatedBy: z.string(),
  });
}

const Order = withAudit(
  z.object({
    id: z.string(),
    total: z.number(),
  })
);

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


Расширение через композицию типов TypeScript

Zod тесно интегрируется с TypeScript через z.infer, что позволяет отражать расширения схем в типах.

const Base = z.object({
  id: z.string(),
});

const Extended = Base.extend({
  name: z.string(),
});

type ExtendedType = z.infer<typeof Extended>;

Любое изменение схемы автоматически отражается в типе, исключая рассинхронизацию между runtime и compile-time представлением.


Ограничения расширения и конфликт типов

При расширении важно учитывать конфликты:

  • несовместимые типы одинаковых ключей приводят к ошибкам
  • объединение литеральных типов требует согласованности
  • merge не выполняет глубокое слияние вложенных объектов

Пример конфликтного расширения:

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

const B = A.extend({
  id: z.number(), // конфликт типов
});

Такие случаи требуют пересмотра структуры схемы, а не механического расширения.


Расширение вложенных структур

При работе со сложными объектами часто требуется расширять вложенные схемы:

const User = z.object({
  profile: z.object({
    name: z.string(),
  }),
});

const ExtendedUser = User.extend({
  profile: User.shape.profile.extend({
    age: z.number(),
  }),
});

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