Переход с io-ts на Zod обычно связан с изменением подхода к описанию и проверке данных в TypeScript-проектах. io-ts опирается на функционально-комбинаторную модель с явным декодированием через Either, тогда как Zod использует более декларативный и императивно-дружественный API с прямой валидацией и выбросом исключений или безопасным результатом через safeParse.
Ключевые различия:
Either<Errors, A>ZodErrorЭто различие влияет на архитектуру слоёв приложения, обработку ошибок и читаемость кода.
io-ts:
import * as t from 'io-ts';
const StringCodec = t.string;
const NumberCodec = t.number;
const BooleanCodec = t.boolean;
Zod:
import { z } from 'zod';
const StringSchema = z.string();
const NumberSchema = z.number();
const BooleanSchema = z.boolean();
В Zod отсутствует необходимость в явных конструкторах codec-уровня, тип выводится автоматически.
io-ts:
const User = t.type({
id: t.number,
name: t.string,
});
Zod:
const User = z.object({
id: z.number(),
name: z.string(),
});
Основное отличие проявляется при работе с расширениями и трансформациями.
io-ts:
const User = t.type({
id: t.number,
name: t.string,
});
const PartialUser = t.partial({
name: t.string,
});
Zod:
const User = z.object({
id: z.number(),
name: z.string().optional(),
});
или:
const User = z.object({
id: z.number(),
name: z.string(),
}).partial();
В Zod опциональность становится частью поля или всей схемы, а не отдельной сущностью.
io-ts:
const A = t.type({ kind: t.literal('a') });
const B = t.type({ kind: t.literal('b') });
const Union = t.union([A, B]);
Zod:
const A = z.object({ kind: z.literal('a') });
const B = z.object({ kind: z.literal('b') });
const Union = z.union([A, B]);
Zod также предоставляет более строгий аналог:
const Discriminated = z.discriminatedUnion('kind', [A, B]);
Это улучшает производительность и точность вывода типов.
io-ts:
const A = t.type({ a: t.string });
const B = t.type({ b: t.number });
const AB = t.intersection([A, B]);
Zod:
const A = z.object({ a: z.string() });
const B = z.object({ b: z.number() });
const AB = z.intersection(A, B);
В Zod интерсекция сохраняет более предсказуемую структуру типов, особенно при сложных композициях.
io-ts:
const StringToNumber = new t.Type<number, string, unknown>(
'StringToNumber',
t.number.is,
(u): u is number => typeof u === 'number',
(u) => (typeof u === 'string' ? parseFloat(u) : NaN),
String
);
Zod:
const StringToNumber = z.string().transform((val) => parseFloat(val));
Ключевое различие — в Zod трансформация является частью цепочки, а не отдельной абстракцией codec.
io-ts:
const result = User.decode(data);
if (result._tag === 'Left') {
console.log(result.left);
} else {
console.log(result.right);
}
Zod:
const result = User.safeParse(data);
if (!result.success) {
console.log(result.error);
} else {
console.log(result.data);
}
Zod также поддерживает:
User.parse(data); // бросает исключение
io-ts ошибки:
PathReporterimport { PathReporter } from 'io-ts/PathReporter';
const errors = PathReporter.report(result);
Zod ошибки:
if (!result.success) {
console.log(result.error.format());
}
или:
result.error.flatten();
Zod предоставляет более читаемую структуру ошибок без дополнительных библиотек.
io-ts:
const UserId = t.brand(
t.number,
(n): n is t.Branded<number, { UserId: symbol }> => n > 0,
'UserId'
);
Zod:
const UserId = z.number().positive().brand<'UserId'>();
Брендинг в Zod менее многословен и лучше интегрирован с системой типов TypeScript.
io-ts:
Композиция требует явного использования intersection,
union, type, partial.
Zod:
const Base = z.object({
id: z.number(),
});
const Extended = Base.extend({
name: z.string(),
});
или:
const Extended = Base.merge(z.object({
name: z.string(),
}));
Zod делает композицию ближе к объектной модели.
io-ts не предоставляет встроенной поддержки асинхронной валидации.
Zod:
const schema = z.string().refine(async (val) => {
return await checkFromDatabase(val);
});
или:
await schema.parseAsync(value);
Это расширяет сценарии использования на внешние API и БД.
io-ts:
type User = t.TypeOf<typeof User>;
Zod:
type User = z.infer<typeof User>;
В Zod вывод типов более прямой и не требует дополнительного
TypeOf слоя.
Каждый codec заменяется на аналогичную zod-схему без изменения бизнес-логики.
// io-ts
User.decode(data)
// Zod
User.safeParse(data)
Функциональные цепочки с Either заменяются на условную обработку результата.
Обработка ошибок переносится на встроенные методы ZodError.
io-ts делает композицию более строгой и функциональной, Zod — более гибкой, но менее формально чистой.
Either-подход требует переписывания обработчиков ошибок.
В io-ts трансформация явно отделена, в Zod часто встроена в схему.
io-ts:
Zod:
После миграции обычно наблюдаются следующие изменения: