Типизация схем в Yup играет ключевую роль при работе с
TypeScript. Библиотека предоставляет механизмы вывода
типов, синхронизации схем с интерфейсами и безопасной валидации данных.
В связке с yupResolver это особенно важно, поскольку форма,
валидатор и итоговые данные должны иметь единый контракт типов.
Базовая схема создаётся через конструкторы:
import * as yup from 'yup'
const schema = yup.object({
name: yup.string().required(),
age: yup.number().required(),
})
Без дополнительных действий TypeScript способен вывести
тип объекта автоматически.
Основной инструмент получения типа из схемы —
InferType.
import * as yup from 'yup'
const userSchema = yup.object({
id: yup.number().required(),
email: yup.string().email().required(),
isAdmin: yup.boolean().default(false),
})
type User = yup.InferType<typeof userSchema>
Результат:
type User = {
id: number
email: string
isAdmin: boolean
}
Поле становится обязательным:
const schema = yup.object({
title: yup.string().required(),
})
type T = yup.InferType<typeof schema>
// {
// title: string
// }
Поле становится необязательным:
const schema = yup.object({
title: yup.string().optional(),
})
type T = yup.InferType<typeof schema>
// {
// title?: string | undefined
// }
Добавляет null:
const schema = yup.object({
avatar: yup.string().nullable(),
})
type T = yup.InferType<typeof schema>
// {
// avatar?: string | null | undefined
// }
При использовании react-hook-form тип схемы обычно
передаётся напрямую в useForm.
import { useForm } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import * as yup from 'yup'
const schema = yup.object({
email: yup.string().email().required(),
password: yup.string().min(6).required(),
})
type FormData = yup.InferType<typeof schema>
const form = useForm<FormData>({
resolver: yupResolver(schema),
})
Теперь:
registerwatchsetValuehandleSubmiterrorsполучают строгую типизацию.
После передачи generic-типа useForm автоматически
типизируется handleSubmit.
const onSub mit = (data: FormData) => {
console.log(data.email)
console.log(data.password)
}
Попытка обратиться к несуществующему полю вызовет ошибку компиляции:
data.username
Допустимо использовать InferType прямо внутри
generic:
const form = useForm<yup.InferType<typeof schema>>({
resolver: yupResolver(schema),
})
Однако отдельный alias обычно делает код читаемее:
type LoginForm = yup.InferType<typeof schema>
В старых версиях Yup использовался SchemaOf.
interface User {
name: string
age: number
}
const schema: yup.SchemaOf<User> = yup.object({
name: yup.string().required(),
age: yup.number().required(),
})
SchemaOf имеет несколько недостатков:
InferTypeСовременный подход:
const schema = yup.object({
name: yup.string().required(),
age: yup.number().required(),
})
type User = yup.InferType<typeof schema>
Частая проблема — расхождение интерфейса и валидатора.
Плохой пример:
interface User {
name: string
age: number
}
const schema = yup.object({
name: yup.string().required(),
})
Схема не валидирует age, хотя интерфейс требует
поле.
Для строгого соответствия можно использовать
ObjectSchema<T>.
interface User {
name: string
age: number
}
const schema: yup.ObjectSchema<User> = yup.object({
name: yup.string().required(),
age: yup.number().required(),
})
Теперь несоответствие интерфейсу вызовет ошибку TypeScript.
const schema = yup.object({
user: yup.object({
name: yup.string().required(),
age: yup.number().required(),
}),
})
type Data = yup.InferType<typeof schema>
Результат:
type Data = {
user: {
name: string
age: number
}
}
const schema = yup.object({
tags: yup.array(yup.string().required()).required(),
})
type Data = yup.InferType<typeof schema>
Результат:
{
tags: string[]
}
const schema = yup.object({
users: yup.array(
yup.object({
id: yup.number().required(),
name: yup.string().required(),
})
),
})
type Data = yup.InferType<typeof schema>
Результат:
{
users?: {
id: number
name: string
}[]
}
const schema = yup.string().nullable()
type T = yup.InferType<typeof schema>
// string | null | undefined
Удаляет undefined.
const schema = yup.string().defined()
type T = yup.InferType<typeof schema>
// string
Также убирает undefined, но дополнительно валидирует
значение.
const schema = yup.string().required()
Разница:
| Метод | Удаляет undefined | Runtime validation |
|---|---|---|
| defined | Да | Нет |
| required | Да | Да |
default() влияет на итоговый тип.
const schema = yup.object({
active: yup.boolean().default(false),
})
type Data = yup.InferType<typeof schema>
Результат:
{
active: boolean
}
Без default() поле могло бы быть optional.
mixed() используется для универсальных значений.
const schema = yup.mixed<string>()
Тип:
string | undefined
const roles = ['admin', 'user', 'moderator'] as const
const schema = yup.object({
role: yup.string().oneOf(roles).required(),
})
type Data = yup.InferType<typeof schema>
Тип:
{
role: "admin" | "user" | "moderator"
}
Yup ограниченно поддерживает union.
Пример:
const schema = yup.object({
type: yup.string().oneOf(['email', 'sms']).required(),
})
Но полноценный discriminated union реализовать сложно.
Для сложных union-типов чаще используют:
ZodValibotArkTypelazy() позволяет строить динамические схемы.
const schema = yup.lazy((value) => {
if (typeof value === 'string') {
return yup.string()
}
return yup.number()
})
Однако TypeScript плохо выводит итоговый тип.
Обычно приходится указывать его вручную:
const schema: yup.Schema<string | number> = yup.lazy(...)
Yup поддерживает context-валидацию.
const schema = yup.string().test(
'custom',
'Error',
function (value) {
const context = this.options.context
return true
}
)
Типизация контекста напрямую ограничена, поэтому часто используется приведение:
type Context = {
minAge: number
}
const context = this.options.context as Context
yupResolver способен выводить тип схемы
автоматически.
const schema = yup.object({
email: yup.string().required(),
})
useForm({
resolver: yupResolver(schema),
})
Но без generic у useForm часть API может потерять
строгую типизацию.
Рекомендуемый вариант:
type FormData = yup.InferType<typeof schema>
useForm<FormData>({
resolver: yupResolver(schema),
})
type FormData = {
email: string
password: string
}
const schema = yup.object({
email: yup.string().required(),
})
Ошибка:
resolver: yupResolver(schema)
Причина — схема не соответствует типу формы.
После типизации формы ошибки становятся безопасными.
formState.errors.email?.message
TypeScript знает:
const schema = yup.object({
items: yup.array(
yup.object({
title: yup.string().required(),
})
),
})
type FormData = yup.InferType<typeof schema>
Теперь useFieldArray получает точный тип элементов.
useFieldArray<FormData>({
name: 'items',
})
Метод strict() отключает преобразование типов.
const schema = yup.number().strict()
Без strict:
schema.validateSync("123")
Результат:
123
Со strict:
ValidationError
На типизацию TypeScript это не влияет, но влияет на runtime-поведение.
const schema = yup.object({
name: yup.string().required(),
})
schema.validate(data, {
stripUnknown: true,
})
Лишние поля будут удалены runtime-валидатором, однако TypeScript всё равно ориентируется только на схему.
const schema = yup.object({
name: yup.string(),
}).noUnknown()
Запрещает неизвестные поля.
Полезно для:
cast() преобразует данные в соответствии со схемой.
const schema = yup.object({
age: yup.number().required(),
})
const result = schema.cast({
age: '42',
})
Тип результата:
{
age: number
}
Альтернатива InferType.
type User = yup.Asserts<typeof schema>
Обычно эквивалентен:
InferType<typeof schema>
import { useForm } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'
import * as yup from 'yup'
const registerSchema = yup.object({
username: yup
.string()
.min(3)
.max(20)
.required(),
email: yup
.string()
.email()
.required(),
password: yup
.string()
.min(8)
.required(),
profile: yup.object({
firstName: yup.string().required(),
lastName: yup.string().required(),
}),
tags: yup.array(
yup.string().required()
).required(),
})
type RegisterForm = yup.InferType<typeof registerSchema>
export function RegisterFormComponent() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<RegisterForm>({
resolver: yupResolver(registerSchema),
})
const onSub mit = (data: RegisterForm) => {
console.log(data)
}
return (
<form onSub mit={handleSubmit(onSubmit)}>
<input {...register('username')} />
<p>{errors.username?.message}</p>
<button type="submit">
Submit
</button>
</form>
)
}
type FormData = yup.InferType<typeof schema>
useForm<FormData>()
Плохой подход:
interface FormData {
email: string
}
при наличии схемы:
const schema = yup.object({
email: yup.string().required(),
})
Лучше:
type FormData = yup.InferType<typeof schema>
const schema: yup.ObjectSchema<User>
Они ухудшают вывод типов и часто требуют ручной типизации.
Yup обеспечивает:
TypeScript обеспечивает:
Эти механизмы работают совместно, но независимо друг от друга.