Типы для частичных схем

В библиотеке Zod работа с частичными схемами строится вокруг преобразования строго описанных структур в их ослабленные варианты, где часть полей становится необязательной, но при этом сохраняется типовая целостность и контроль данных на уровне TypeScript.

Основной механизм частичных схем реализуется через метод .partial(), применяемый к объектным схемам.

import { z } from "zod";

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

const PartialUserSchema = UserSchema.partial();

Результатом является новая схема, в которой каждое поле исходного объекта автоматически преобразуется в опциональное:

// эквивалентная структура
{
  id?: string;
  name?: string;
  email?: string;
  age?: number;
}

Важно, что partial() не удаляет ограничения типов, а лишь изменяет статус обязательности полей, сохраняя валидаторы каждого поля.

Поверхностная частичность и её ограничения

Метод .partial() работает только на первом уровне вложенности. Вложенные объекты не становятся частичными автоматически:

const Schema = z.object({
  profile: z.object({
    bio: z.string(),
    website: z.string()
  })
});

const PartialSchema = Schema.partial();

Результат:

{
  profile?: {
    bio: string;
    website: string;
  }
}

Внутренний объект profile остаётся полностью строгим.

Глубокая частичность

Для сценариев, где требуется рекурсивное ослабление структуры, используется глубокая частичность. В стандартной практике Zod это достигается через комбинацию утилит или пользовательских рекурсивных типов, поскольку поведение может зависеть от версии библиотеки.

Типовой подход через TypeScript:

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

Использование вместе с выводом типов Zod:

type User = z.infer<typeof UserSchema>;
type PartialUser = DeepPartial<User>;

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

Частичные схемы и типизация Zod

Zod предоставляет два основных уровня типизации:

  • z.infer<typeof Schema> — тип результата после валидации
  • z.input<typeof Schema> — тип входных данных до трансформаций

При использовании .partial() поведение типов изменяется автоматически:

type User = z.infer<typeof UserSchema>;
type PartialUser = z.infer<typeof PartialUserSchema>;

PartialUser эквивалентен Partial<User> на уровне TypeScript, но с сохранением оригинальных валидаторов в runtime.

Разница между Partial и optional полями

Часто возникает путаница между optional() и partial():

optional()

Применяется к конкретному полю:

z.string().optional();

partial()

Применяется ко всей объектной схеме:

z.object({...}).partial();

Ключевое различие:

  • optional() влияет на одно поле
  • partial() трансформирует структуру целиком

Частичные схемы в контексте обновлений данных

Частичные схемы широко используются для PATCH-операций, где требуется обновление только части сущности:

const UpdateUserSchema = UserSchema.partial();

function updateUser(data: z.infer<typeof UpdateUserSchema>) {
  // data может содержать любое подмножество полей User
}

Такой подход исключает необходимость передачи полной модели и снижает связанность API.

Комбинация partial с pick и omit

Частичные схемы часто комбинируются с ограничением полей:

pick + partial

const NameEmailSchema = UserSchema.pick({
  name: true,
  email: true
}).partial();

Результат: оба поля становятся необязательными, но остальные полностью исключены.

omit + partial

const WithoutIdPartial = UserSchema.omit({
  id: true
}).partial();

Здесь частичность применяется только к оставшимся полям.

Расширение схем и частичность

При использовании .extend() частичность применяется после композиции:

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

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

Все поля итоговой схемы становятся опциональными.

Взаимодействие с default и undefined

Частичные схемы влияют только на наличие поля, но не подменяют значение по умолчанию:

const Schema = z.object({
  name: z.string().default("anon")
}).partial();

В этом случае:

  • отсутствие name допустимо
  • но default() применяется только если поле присутствует или обрабатывается через parse

Важное различие:

  • undefined как значение
  • отсутствие ключа в объекте

Zod различает эти состояния при валидации.

Преобразование частичных схем

Частичные схемы часто используются вместе с preprocess, когда входные данные нормализуются перед проверкой:

const Schema = z.object({
  name: z.string()
}).partial().preprocess((val) => {
  return val ?? {};
}, z.any());

Такой подход позволяет безопасно обрабатывать неполные структуры без ошибок парсинга.

Типовые ошибки при работе с partial

Потеря строгой структуры вложенных объектов

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

Конфликт с required полями после композиции

При объединении схем порядок операций влияет на итоговую типизацию:

  • .extend().partial() — всё опционально
  • .partial().extend() — новые поля могут остаться обязательными

Несоответствие input/output типов

z.input может оставаться более строгим, чем z.output, особенно при трансформациях и default-значениях.

Частичные схемы и union-структуры

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

const Schema = z.union([
  z.object({ type: z.literal("a"), value: z.string() }),
  z.object({ type: z.literal("b"), count: z.number() })
]).partial();

Каждый объект внутри union становится частичным независимо.

Рекурсивные структуры и частичность

Для деревьев и графов частичность требует особого подхода, поскольку стандартный .partial() не обрабатывает рекурсию:

const Node: z.ZodType<any> = z.lazy(() =>
  z.object({
    value: z.string(),
    children: z.array(Node)
  })
);

Применение частичности к таким схемам требует либо ручной трансформации, либо обобщённых типов.

Итоговая роль partial в типовой системе Zod

Частичные схемы в Zod формируют слой адаптации между строгими моделями данных и гибкими структурами ввода. Они позволяют:

  • ослаблять контракты без потери типовой информации
  • описывать частичные обновления данных
  • согласовывать runtime-валидацию с TypeScript-типами
  • управлять степенью строгости структуры на уровне схемы