Работа с типами в 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 — stringname — stringemail — 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(),
})
);
Этот паттерн позволяет стандартизировать добавление служебных полей.
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 представлением.
При расширении важно учитывать конфликты:
Пример конфликтного расширения:
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(),
}),
});
Это позволяет точечно модифицировать глубоко вложенные структуры без пересоздания всей схемы.