Миграция с io-ts

Переход с io-ts на Zod обычно связан с изменением подхода к описанию и проверке данных в TypeScript-проектах. io-ts опирается на функционально-комбинаторную модель с явным декодированием через Either, тогда как Zod использует более декларативный и императивно-дружественный API с прямой валидацией и выбросом исключений или безопасным результатом через safeParse.

Ключевые различия:

  • io-ts строится вокруг концепции codecs
  • Zod строится вокруг схем (schemas)
  • io-ts возвращает Either<Errors, A>
  • Zod возвращает либо значение, либо 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-уровня, тип выводится автоматически.


Объекты: codec vs schema

io-ts:

const User = t.type({
  id: t.number,
  name: t.string,
});

Zod:

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

Основное отличие проявляется при работе с расширениями и трансформациями.


Опциональные поля и partial

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 опциональность становится частью поля или всей схемы, а не отдельной сущностью.


Union-типы

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]);

Это улучшает производительность и точность вывода типов.


Intersection-типы

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 интерсекция сохраняет более предсказуемую структуру типов, особенно при сложных композициях.


Преобразование типов (map / transform)

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 ошибки:

  • структурированы через PathReporter
  • требуют дополнительной обработки
import { 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 делает композицию ближе к объектной модели.


Async-валидация

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 слоя.


Миграционные паттерны

1. Прямое сопоставление схем

Каждый codec заменяется на аналогичную zod-схему без изменения бизнес-логики.

2. Замена decode на safeParse

// io-ts
User.decode(data)

// Zod
User.safeParse(data)

3. Упрощение pipeline обработки

Функциональные цепочки с Either заменяются на условную обработку результата.

4. Удаление PathReporter

Обработка ошибок переносится на встроенные методы ZodError.


Частые проблемы при переходе

Потеря явной функциональной композиции

io-ts делает композицию более строгой и функциональной, Zod — более гибкой, но менее формально чистой.

Изменение модели ошибок

Either-подход требует переписывания обработчиков ошибок.

Неявная трансформация данных

В io-ts трансформация явно отделена, в Zod часто встроена в схему.


Сравнение архитектурного влияния

io-ts:

  • строгая функциональная модель
  • явное разделение decode/encode
  • больше шаблонного кода
  • высокая предсказуемость

Zod:

  • декларативная модель
  • интеграция с TypeScript inference
  • компактность
  • гибкость трансформаций и refinement

Результирующие изменения в кодовой базе

После миграции обычно наблюдаются следующие изменения:

  • уменьшение количества вспомогательных типов
  • снижение объёма boilerplate-кода
  • упрощение обработки ошибок
  • усиление роли runtime-валидации в доменной логике
  • более тесная интеграция схем с UI-формами и API-слоями