Одной из ключевых возможностей при работе с 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;
}[];
};
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>;
Это позволяет строить типы по принципу модульности, где каждая схема становится источником собственного типа.
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 схема 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() не всегда отражаются в
типах;.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.