z.infer и его применение

В Zod механизм z.infer представляет собой один из ключевых инструментов связки runtime-валидации и статической типизации в TypeScript. Его основная задача — извлечение TypeScript-типа непосредственно из объявленной схемы, что устраняет дублирование описаний типов и схем валидации.


Базовый принцип работы z.infer

Каждая схема Zod описывает структуру данных на уровне исполнения (runtime). Параллельно TypeScript способен выводить тип на основе этой схемы через утилиту:

import { z } from "zod";

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

type User = z.infer<typeof UserSchema>;

В результате User эквивалентен:

type User = {
  id: number;
  name: string;
  email: string;
};

Таким образом, z.infer устраняет необходимость ручного описания интерфейсов при наличии схемы.


Типизация примитивов и базовых структур

Примитивные типы

const AgeSchema = z.number();

type Age = z.infer<typeof AgeSchema>; // number
const NameSchema = z.string();

type Name = z.infer<typeof NameSchema>; // string

Объекты

const ProductSchema = z.object({
  title: z.string(),
  price: z.number(),
  inStock: z.boolean(),
});

type Product = z.infer<typeof ProductSchema>;

Массивы

const TagsSchema = z.array(z.string());

type Tags = z.infer<typeof TagsSchema>; // string[]

Union-типы и z.infer

Zod активно поддерживает объединения, и z.infer корректно отображает их в TypeScript.

const IdSchema = z.union([z.string(), z.number()]);

type Id = z.infer<typeof IdSchema>; // string | number

Discriminated unions

Особенно важный случай — дискриминируемые объединения.

const ShapeSchema = z.discriminatedUnion("type", [
  z.object({
    type: z.literal("circle"),
    radius: z.number(),
  }),
  z.object({
    type: z.literal("square"),
    size: z.number(),
  }),
]);

type Shape = z.infer<typeof ShapeSchema>;

Результирующий тип:

type Shape =
  | { type: "circle"; radius: number }
  | { type: "square"; size: number };

Optional, nullable и default значения

Optional поля

const Schema = z.object({
  nickname: z.string().optional(),
});

type T = z.infer<typeof Schema>;
// { nickname?: string }

Nullable поля

const Schema = z.object({
  middleName: z.string().nullable(),
});

type T = z.infer<typeof Schema>;
// { middleName: string | null }

Default значения

const Schema = z.object({
  role: z.string().default("user"),
});

type T = z.infer<typeof Schema>;
// { role: string }

Важно: TypeScript-тип отражает уже «развернутую» форму, а не факт наличия default на runtime.


Отличие z.infer от ручных интерфейсов

Традиционный подход:

interface User {
  id: number;
  name: string;
}

И схема отдельно:

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

Проблема — рассинхронизация.

Подход с z.infer:

type User = z.infer<typeof UserSchema>;

Схема становится единственным источником истины.


Вложенные структуры

const CommentSchema = z.object({
  text: z.string(),
});

const PostSchema = z.object({
  title: z.string(),
  comments: z.array(CommentSchema),
});

type Post = z.infer<typeof PostSchema>;

Результат:

type Post = {
  title: string;
  comments: { text: string }[];
};

Transform и влияние на infer

transform изменяет runtime-значение, но влияет и на итоговый тип.

const Schema = z.string().transform((val) => val.length);

type T = z.infer<typeof Schema>; // number

Несмотря на исходный string, итоговый тип — number.

Это критически важный момент: z.infer отражает результат трансформаций, а не только входную форму.


Preprocess и изменение входного типа

const Schema = z.preprocess((val) => {
  if (typeof val === "string") return Number(val);
  return val;
}, z.number());

type T = z.infer<typeof Schema>; // number

Тип определяется финальной схемой (z.number()), а не входным значением.


Effects и refinement

refine

const Schema = z.string().refine((val) => val.length > 5);

type T = z.infer<typeof Schema>; // string

Refine не изменяет тип, так как добавляет только runtime-ограничение.


superRefine

const Schema = z.object({
  password: z.string(),
}).superRefine((data, ctx) => {
  if (data.password.length < 8) {
    ctx.addIssue({ code: "custom", message: "Too short" });
  }
});

type T = z.infer<typeof Schema>;

Тип остаётся структурным, без изменений.


Рекурсивные схемы

type Category = {
  name: string;
  children?: Category[];
};

const CategorySchema: z.ZodType<Category> = z.lazy(() =>
  z.object({
    name: z.string(),
    children: z.array(CategorySchema).optional(),
  })
);

type T = z.infer<typeof CategorySchema>;

z.infer корректно разворачивает рекурсивные структуры, сохраняя типизацию вложенности.


Ограничения z.infer

1. Потеря некоторых runtime-деталей

TypeScript-тип не отражает:

  • кастомные ошибки
  • сообщения валидации
  • сложные зависимости между полями

2. Transform и различие вход/выход

const Schema = z.string().transform(Number);

Вход: string Выход: number

Но исходный тип string полностью теряется на уровне z.infer.


3. Complex effects

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

z.object({...}).merge(...).partial().strict().transform(...)

Результирующий z.infer может быть глубоко вычисленным и тяжёлым для IDE.


Практическое применение в архитектуре

API слой

const RequestSchema = z.object({
  userId: z.string(),
});

type RequestDTO = z.infer<typeof RequestSchema>;

Использование одного источника данных для:

  • валидации входа
  • типизации обработчиков
  • документации API

Сервисный слой

const UserSchema = z.object({
  id: z.string(),
  role: z.enum(["admin", "user"]),
});

type User = z.infer<typeof UserSchema>;

Типы напрямую синхронизированы с бизнес-логикой.


Репозитории и базы данных

const DbUserSchema = z.object({
  id: z.string(),
  createdAt: z.date(),
});

type DbUser = z.infer<typeof DbUserSchema>;

Позволяет согласовать ORM-слой и runtime-валидацию.


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

function parse<T extends z.ZodTypeAny>(schema: T, data: unknown): z.infer<T> {
  return schema.parse(data);
}

Здесь z.infer<T> обеспечивает корректное извлечение типа для любого переданного schema.


Вывод типовой модели через составные схемы

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

const ExtendedSchema = BaseSchema.extend({
  name: z.string(),
});

type Entity = z.infer<typeof ExtendedSchema>;

Результат автоматически включает расширения.


Итоговая роль z.infer в типовой системе

Механизм выступает связующим звеном между:

  • runtime-валидацией данных
  • статической системой типов TypeScript
  • декларативным описанием структуры данных

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