Миграция с Yup

Переход с Yup на Zod в проектах на JavaScript и TypeScript чаще всего связан с необходимостью усиления типизации, повышения предсказуемости схем валидации и более тесной интеграции с возможностями TypeScript. Архитектурно обе библиотеки решают одну задачу — описание и проверку структур данных, однако различаются философией, подходами к типам и способом построения схем.

В экосистеме валидации данных Yup долгое время использовался как стандарт де-факто для декларативного описания схем. Он ориентирован на удобство и цепочный API, однако в крупных TypeScript-проектах проявляются ограничения:

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

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

Концептуальные различия Yup и Zod

Zod строится на иной концепции: схема является одновременно источником истины для типов и рантайм-валидации. Основные различия проявляются на уровне архитектуры:

Подход к типам

  • Yup: типы выводятся опционально и требуют InferType, часто не совпадают с реальной схемой при сложных композициях
  • Zod: типы извлекаются напрямую из схемы через z.infer, обеспечивая строгую синхронизацию

Иммутабельность

  • Yup использует мутабельные цепочки
  • Zod строит иммутабельные схемы, каждая операция возвращает новую сущность

API

  • Yup: fluent API с методами .string().required()
  • Zod: функционально-декларативный стиль z.string().min(1)

Ошибки

  • Yup возвращает массивы ошибок с менее структурированным описанием
  • Zod формирует структурированные ошибки с кодами и путями

Подготовка к миграции

Перед заменой библиотеки важно зафиксировать существующие схемы и их поведение. Обычно выделяются следующие категории:

  • простые примитивные схемы (строки, числа, булевы значения)
  • вложенные объекты
  • массивы и списки
  • условные схемы (when)
  • асинхронные проверки (уникальность, запросы к API)

Дополнительно фиксируются типы, используемые в бизнес-логике, поскольку именно они будут сопоставляться с новой системой типов.

Базовое сопоставление типов

Yup Zod
yup.string() z.string()
yup.number() z.number()
yup.boolean() z.boolean()
yup.array() z.array()
yup.object() z.object()
yup.mixed() z.any() или z.unknown()

Ключевое отличие проявляется в методах модификации:

  • Yup: .required(), .nullable()
  • Zod: .optional(), .nullable(), .default()

Переписывание простых схем

Базовая строковая схема:

Yup:

yup.string().required().min(3)

Zod:

z.string().min(3)

В Zod обязательность по умолчанию является частью контракта: отсутствие .optional() означает обязательное поле.

Числовая схема:

Yup:

yup.number().positive().integer()

Zod:

z.number().positive().int()

Разница заключается в более строгом разделении методов и отсутствии неоднозначных комбинаций.

Объекты и вложенные структуры

Сложные структуры в Yup часто строятся через yup.object().shape({}).

Пример:

yup.object({
  user: yup.object({
    name: yup.string().required(),
    age: yup.number().min(18)
  })
})

Эквивалент в Zod:

z.object({
  user: z.object({
    name: z.string(),
    age: z.number().min(18)
  })
})

Ключевое отличие проявляется в типизации: Zod автоматически выводит точный тип вложенного объекта без дополнительных утилит.

Массивы и коллекции

Yup:

yup.array().of(yup.string().required())

Zod:

z.array(z.string())

В Zod отсутствие необходимости явно указывать required упрощает композицию схем.

Условная логика

В Yup часто применяется when:

yup.string().when("role", {
  is: "admin",
  then: schema => schema.required()
})

В Zod условная логика реализуется через z.discriminatedUnion или refine:

z.object({
  role: z.string(),
  value: z.string().optional()
}).refine(data => data.role !== "admin" || !!data.value)

Более строгий вариант:

z.discriminatedUnion("role", [
  z.object({ role: z.literal("admin"), value: z.string() }),
  z.object({ role: z.literal("user") })
])

Асинхронная валидация

В Yup:

yup.string().test("unique", async value => {
  return await checkUnique(value)
})

В Zod:

z.string().refine(async value => {
  return await checkUnique(value)
})

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

Типизация и интеграция с TypeScript

Одним из ключевых факторов перехода является строгая интеграция с TypeScript.

В Zod тип извлекается напрямую:

const schema = z.object({
  name: z.string(),
  age: z.number()
})

type User = z.infer<typeof schema>

В Yup требуется дополнительный шаг:

type User = yup.InferType<typeof schema>

Однако различие не только синтаксическое. В Zod тип всегда полностью соответствует схеме без побочных расхождений.

Ошибки и их структура

Yup возвращает ошибки в виде массива строк или объектов с минимальной структурой.

Zod формирует детализированное дерево ошибок:

{
  issues: [
    {
      path: ["user", "name"],
      message: "Invalid input",
      code: "invalid_type"
    }
  ]
}

Это упрощает построение UI-валидации, особенно в формах с вложенной структурой.

Частичная совместимость и стратегии миграции

Переход редко выполняется одномоментно. Обычно используется поэтапный подход:

1. Обёртывание старых схем

Сначала Yup-схемы сохраняются, а новые модули пишутся на Zod.

2. Параллельная валидация

Обе библиотеки применяются одновременно для критических участков системы.

3. Замена доменов

Модули делятся на домены: формы, API, модели данных. Каждый домен мигрируется отдельно.

4. Удаление Yup

После полного покрытия тестами Yup исключается из зависимостей.

Переписывание форм

В связке с формами часто используется React Hook Form.

Yup:

resolver: yupResolver(schema)

Zod:

resolver: zodResolver(schema)

Zod обеспечивает более строгую типизацию формы, особенно при использовании generics.

Композиция схем

Одним из ключевых преимуществ Zod является композиция:

const base = z.object({
  id: z.string()
})

const extended = base.extend({
  name: z.string()
})

В Yup аналог требует пересоздания объекта через .concat, что менее прозрачно.

Пересечение и объединение типов

Zod предоставляет встроенные операции:

z.intersection(schemaA, schemaB)
z.union([schemaA, schemaB])

В Yup подобные операции реализуются менее явно и требуют кастомных решений.

Работа с default-значениями

Yup:

yup.string().default("guest")

Zod:

z.string().default("guest")

Однако Zod возвращает значение с учётом типизации результата, включая корректное выведение string вместо string | undefined.

Ошибки миграции и типовые проблемы

При переходе часто возникают следующие сложности:

  • несоответствие nullable и optional
  • различие в обработке пустых строк
  • асинхронные валидаторы, не переведённые в parseAsync
  • расхождения в transform
  • потеря поведения stripUnknown

Zod по умолчанию более строг в отношении неизвестных полей, что требует явного использования:

z.object({...}).passthrough()

или

z.object({...}).strict()

Работа с трансформациями

Yup:

transform(value => value.trim())

Zod:

z.string().transform(value => value.trim())

Zod сохраняет цепочку типизации даже после трансформации, что снижает риск потери корректного типа.

Структурные преимущества после миграции

После перехода на Zod структура валидации становится более предсказуемой:

  • единый источник типов и правил
  • отсутствие дублирования интерфейсов
  • более строгая обработка ошибок
  • явное разделение sync/async логики
  • улучшенная композиция схем

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