Работа с generic типами

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


Базовая модель generics в Zod

Базовый класс для всех схем в Zod представлен как:

ZodType<Output, Def, Input>

где:

  • Output — тип, который получается после успешного парсинга;
  • Input — тип входных данных, которые допустимы до трансформации;
  • Def — внутренняя структура определения схемы (обычно не используется напрямую).

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


Универсальные схемы через дженерики TypeScript

Одним из основных применений generic-подхода является создание фабрик схем.

import { z } from "zod";

function createNullableSchema<T extends z.ZodTypeAny>(schema: T) {
  return schema.nullable();
}

Здесь T extends z.ZodTypeAny обеспечивает, что в функцию может быть передана любая Zod-схема, а возвращаемое значение сохраняет корректную типизацию.


Параметризация объектов

Generic-подход часто применяется при создании объектов со строго типизированной структурой:

function createEntitySchema<T extends z.ZodRawShape>(shape: T) {
  return z.object(shape);
}

Использование:

const userSchema = createEntitySchema({
  id: z.string(),
  age: z.number(),
});

Тип T фиксирует структуру объекта, а Zod автоматически выводит результирующий тип.


Обобщённые фабрики с трансформацией

Generic-типы особенно важны при комбинировании transform и refine.

function withParsedNumber<T extends z.ZodTypeAny>(schema: T) {
  return schema.transform((val) => Number(val));
}

Типизация при этом сохраняет связь между входным и выходным типом, если трансформация явно описана.


ZodTypeAny как универсальный ограничитель

z.ZodTypeAny используется как верхнеуровневое ограничение для любых схем:

function wrapSchema<T extends z.ZodTypeAny>(schema: T) {
  return schema.optional();
}

Этот тип играет ключевую роль в построении универсальных утилит, поскольку охватывает:

  • ZodString
  • ZodNumber
  • ZodObject
  • ZodArray
  • и любые композиции схем

Generic-типы в сложных композициях схем

При построении вложенных структур generics позволяют сохранять типовую целостность:

function createResponseSchema<T extends z.ZodTypeAny>(dataSchema: T) {
  return z.object({
    success: z.boolean(),
    data: dataSchema,
  });
}

Результирующий тип автоматически зависит от переданного dataSchema, что позволяет создавать типобезопасные API-ответы.


Ограничение generics через infer-логику

Zod активно использует TypeScript inference, который усиливается generic-параметрами:

type Infer<T extends z.ZodTypeAny> = z.infer<T>;

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


Generic-типы и discriminated unions

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

function createEventSchema<T extends string>(type: T) {
  return z.object({
    type: z.literal(type),
    payload: z.any(),
  });
}

Комбинация таких схем формирует строгие discriminated unions.


Переиспользуемые generic-композиции

Типичная архитектура валидации строится через цепочку generic-функций:

function withMeta<T extends z.ZodTypeAny>(schema: T) {
  return z.object({
    meta: z.object({
      timestamp: z.number(),
    }),
    data: schema,
  });
}

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


Ограничения и особенности вывода типов

При усложнении generic-цепочек TypeScript может терять точность вывода. Это особенно заметно при:

  • глубокой вложенности transform
  • множественных refine
  • композиции через merge и intersection

Для стабилизации типов применяется явное указание ZodType<Output, Def, Input>.


Явное описание generic-схем

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

const schema: z.ZodType<{ id: string }, any, unknown> = z.object({
  id: z.string(),
});

Такой подход фиксирует контракт и исключает неоднозначность в типизации.


Generic-типы в расширении стандартных схем

Создание расширяемых схем базируется на комбинировании generics и extend:

function extendSchema<T extends z.ZodRawShape>(base: T) {
  return z.object(base).extend({
    createdAt: z.date(),
  });
}

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


Композиция generic-функций

Сложные схемы часто формируются через композицию функций:

function pipeSchema<T extends z.ZodTypeAny>(schema: T) {
  return withMeta(schema).optional();
}

Каждый уровень композиции сохраняет связь типов, если generics корректно протянуты через цепочку.


Типизация динамических схем

Generic-типы позволяют моделировать динамическое поведение схем:

function conditionalSchema<T extends z.ZodTypeAny>(
  condition: boolean,
  schema: T
) {
  return condition ? schema : z.any();
}

Хотя структура становится динамической, типовая часть сохраняется через T.


Проблемы избыточной генерализации

Чрезмерное использование generics приводит к:

  • усложнению вывода типов TypeScript
  • увеличению времени компиляции
  • снижению читаемости схем
  • потере конкретики в inference

Баланс достигается через ограничение универсальности и явное указание типов в критических местах.


Generic-типы и безопасность контрактов

Основная ценность generics в Zod заключается в строгом связывании:

  • входных данных
  • схемы валидации
  • выходного типа TypeScript

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