В Zod механизм z.infer представляет собой один из
ключевых инструментов связки runtime-валидации и статической типизации в
TypeScript. Его основная задача — извлечение TypeScript-типа
непосредственно из объявленной схемы, что устраняет дублирование
описаний типов и схем валидации.
Каждая схема 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[]
Zod активно поддерживает объединения, и z.infer
корректно отображает их в TypeScript.
const IdSchema = z.union([z.string(), z.number()]);
type Id = z.infer<typeof IdSchema>; // string | number
Особенно важный случай — дискриминируемые объединения.
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 };
const Schema = z.object({
nickname: z.string().optional(),
});
type T = z.infer<typeof Schema>;
// { nickname?: string }
const Schema = z.object({
middleName: z.string().nullable(),
});
type T = z.infer<typeof Schema>;
// { middleName: string | null }
const Schema = z.object({
role: z.string().default("user"),
});
type T = z.infer<typeof Schema>;
// { role: string }
Важно: TypeScript-тип отражает уже «развернутую» форму, а не факт наличия default на runtime.
Традиционный подход:
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 изменяет runtime-значение, но влияет и на
итоговый тип.
const Schema = z.string().transform((val) => val.length);
type T = z.infer<typeof Schema>; // number
Несмотря на исходный string, итоговый тип —
number.
Это критически важный момент: z.infer отражает результат
трансформаций, а не только входную форму.
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()), а не
входным значением.
const Schema = z.string().refine((val) => val.length > 5);
type T = z.infer<typeof Schema>; // string
Refine не изменяет тип, так как добавляет только runtime-ограничение.
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 корректно разворачивает рекурсивные структуры,
сохраняя типизацию вложенности.
TypeScript-тип не отражает:
const Schema = z.string().transform(Number);
Вход: string Выход: number
Но исходный тип string полностью теряется на уровне
z.infer.
При сложных композициях схем тип может становиться трудно читаемым:
z.object({...}).merge(...).partial().strict().transform(...)
Результирующий z.infer может быть глубоко вычисленным и
тяжёлым для IDE.
const RequestSchema = z.object({
userId: z.string(),
});
type RequestDTO = z.infer<typeof RequestSchema>;
Использование одного источника данных для:
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-валидацию.
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>;
Результат автоматически включает расширения.
Механизм выступает связующим звеном между:
Он формирует единый источник истины, устраняя дублирование интерфейсов и схем, а также обеспечивая согласованность между исполнением и компиляцией.