Infer типов из схемы Yup

Одной из ключевых возможностей при работе с Yup становится извлечение TypeScript-типов непосредственно из схемы валидации. Это позволяет устранить дублирование типов данных между схемой и бизнес-логикой, а также снижает риск рассинхронизации формы и её валидации. Основой механизма является утилитный тип yup.InferType, который анализирует структуру схемы и строит соответствующий тип данных.


Базовый принцип вывода типов

Yup описывает структуру данных через цепочку валидаторов:

import * as yup from "yup";

const schema = yup.object({
  name: yup.string().required(),
  age: yup.number().required().min(18),
});

Для получения TypeScript-типа используется:

type FormData = yup.InferType<typeof schema>;

Результирующий тип будет эквивалентен:

type FormData = {
  name: string;
  age: number;
};

Ключевой момент заключается в том, что TypeScript анализирует именно схему, а не ручные аннотации. Это делает типизацию производной от источника истины — валидационного слоя.


Вложенные объекты

Yup поддерживает глубокую вложенность объектов, и InferType корректно отражает эту структуру.

const schema = yup.object({
  user: yup.object({
    id: yup.string().required(),
    profile: yup.object({
      firstName: yup.string().required(),
      lastName: yup.string().required(),
    }),
  }),
});

Типизация:

type FormData = yup.InferType<typeof schema>;

Результат:

type FormData = {
  user: {
    id: string;
    profile: {
      firstName: string;
      lastName: string;
    };
  };
};

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


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

Работа с массивами в Yup строится через yup.array(). Инференс сохраняет тип элементов.

const schema = yup.object({
  tags: yup.array().of(yup.string().required()),
});

Тип:

type FormData = {
  tags: string[];
};

При использовании сложных объектов внутри массива:

const schema = yup.object({
  users: yup.array().of(
    yup.object({
      id: yup.number().required(),
      email: yup.string().email().required(),
    })
  ),
});

Результат:

type FormData = {
  users: {
    id: number;
    email: string;
  }[];
};

Optional, required и nullable поля

Yup неявно различает обязательность через .required(), но TypeScript-тип строится с учётом конфигурации схемы.

const schema = yup.object({
  nickname: yup.string().notRequired(),
  email: yup.string().required(),
});

Тип:

type FormData = {
  nickname?: string;
  email: string;
};

При использовании nullable() появляется возможность null:

const schema = yup.object({
  middleName: yup.string().nullable(),
});

Тип:

type FormData = {
  middleName: string | null;
};

Комбинация notRequired() и nullable() даёт наиболее гибкий вариант:

yup.string().nullable().notRequired();

Тип:

string | null | undefined

Значения по умолчанию

Метод .default() влияет на итоговую типизацию, расширяя тип значением по умолчанию.

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

Тип:

type FormData = {
  role: string;
};

Несмотря на наличие дефолта, итоговый тип остаётся string, поскольку TypeScript не моделирует runtime-инициализацию как часть сигнатуры данных формы. Это важное ограничение: дефолт влияет на выполнение, но не превращает поле в string | undefined.


Объединение схем и реиспользование типов

При построении больших форм часто используются фрагменты схем, которые объединяются в итоговую структуру.

const addressSchema = yup.object({
  city: yup.string().required(),
  zip: yup.string().required(),
});

const userSchema = yup.object({
  name: yup.string().required(),
  address: addressSchema,
});

Типизация сохраняет композицию:

type Address = yup.InferType<typeof addressSchema>;
type User = yup.InferType<typeof userSchema>;

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


Условные схемы и их влияние на inference

Yup поддерживает условную логику через .when(), однако TypeScript-инференс в таких случаях становится ограниченным.

const schema = yup.object({
  isAdmin: yup.boolean(),
  adminCode: yup.string().when("isAdmin", {
    is: true,
    then: (schema) => schema.required(),
    otherwise: (schema) => schema.notRequired(),
  }),
});

Тип:

type FormData = {
  isAdmin: boolean;
  adminCode?: string;
};

TypeScript не может отразить зависимость adminCode от isAdmin как дискриминируемое объединение без дополнительной ручной типизации. Это один из ключевых случаев, где автоматический inference уступает статически выраженной модели.


Приведение типов через кастомные трансформации

Метод .transform() позволяет изменять входные данные перед валидацией, но TypeScript не всегда может вывести итоговый тип корректно.

const schema = yup.object({
  price: yup
    .number()
    .transform((value, originalValue) =>
      typeof originalValue === "string" ? parseFloat(originalValue) : value
    ),
});

Тип остаётся:

type FormData = {
  price: number;
};

Несмотря на возможные string-значения на входе, inference отражает только финальный результат после трансформации. Это означает, что Yup рассматривается как слой нормализации данных.


Интеграция с React Hook Form Resolver

В экосистеме React Hook Form схема Yup часто используется через @hookform/resolvers/yup. Именно здесь inference становится критически важным, поскольку связывает форму и валидацию.

import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";

type FormData = yup.InferType<typeof schema>;

const form = useForm<FormData>({
  resolver: yupResolver(schema),
});

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


Ограничения вывода типов

Несмотря на высокую точность, inference имеет ряд ограничений:

  • условные схемы не формируют зависимые типы;
  • сложные кастомные .transform() не всегда отражаются в типах;
  • динамическая генерация схем не поддаётся статическому анализу;
  • union-логика внутри .oneOf() не всегда корректно транслируется в TypeScript union.

Пример проблемной конструкции:

const schema = yup.object({
  value: yup.mixed().oneOf(["a", "b", "c"]),
});

Ожидаемый тип:

"a" | "b" | "c"

Но в некоторых конфигурациях inference может деградировать до any или string, особенно при использовании mixed() без строгой типизации.


Практика построения строго типизированных схем

Наиболее стабильный подход заключается в том, чтобы:

  • всегда начинать с yup.object();
  • избегать mixed() без необходимости;
  • использовать as const при работе с union-значениями;
  • явно типизировать сложные условные структуры.

Пример:

const ROLES = ["admin", "user", "guest"] as const;

const schema = yup.object({
  role: yup.mixed<typeof ROLES[number]>().oneOf(ROLES).required(),
});

Тип:

type FormData = {
  role: "admin" | "user" | "guest";
};

Такой подход сохраняет баланс между гибкостью Yup и строгостью TypeScript.