Typed schema

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


Типизация схем в Yup

Базовая схема создаётся через конструкторы:

import * as yup from 'yup'

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

Без дополнительных действий TypeScript способен вывести тип объекта автоматически.


InferType

Основной инструмент получения типа из схемы — 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
}

Особенности InferType

required()

Поле становится обязательным:

const schema = yup.object({
  title: yup.string().required(),
})

type T = yup.InferType<typeof schema>

// {
//   title: string
// }

optional()

Поле становится необязательным:

const schema = yup.object({
  title: yup.string().optional(),
})

type T = yup.InferType<typeof schema>

// {
//   title?: string | undefined
// }

nullable()

Добавляет null:

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

type T = yup.InferType<typeof schema>

// {
//   avatar?: string | null | undefined
// }

Типизация useForm

При использовании 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),
})

Теперь:

  • register
  • watch
  • setValue
  • handleSubmit
  • errors

получают строгую типизацию.


Типизация submit handler

После передачи generic-типа useForm автоматически типизируется handleSubmit.

const onSub mit = (data: FormData) => {
  console.log(data.email)
  console.log(data.password)
}

Попытка обратиться к несуществующему полю вызовет ошибку компиляции:

data.username

Автоматический вывод типа без отдельного type

Допустимо использовать InferType прямо внутри generic:

const form = useForm<yup.InferType<typeof schema>>({
  resolver: yupResolver(schema),
})

Однако отдельный alias обычно делает код читаемее:

type LoginForm = yup.InferType<typeof schema>

SchemaOf

В старых версиях Yup использовался SchemaOf.

interface User {
  name: string
  age: number
}

const schema: yup.SchemaOf<User> = yup.object({
  name: yup.string().required(),
  age: yup.number().required(),
})

Проблемы SchemaOf

SchemaOf имеет несколько недостатков:

  • сложнее работает с union
  • хуже поддерживает optional-поля
  • создаёт громоздкие generic-конструкции
  • постепенно вытесняется 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

Для строгого соответствия можно использовать ObjectSchema<T>.

interface User {
  name: string
  age: number
}

const schema: yup.ObjectSchema<User> = yup.object({
  name: yup.string().required(),
  age: yup.number().required(),
})

Теперь несоответствие интерфейсу вызовет ошибку TypeScript.


Типизация nested объектов

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
  }[]
}

Типизация nullable и defined

nullable()

const schema = yup.string().nullable()

type T = yup.InferType<typeof schema>

// string | null | undefined

defined()

Удаляет undefined.

const schema = yup.string().defined()

type T = yup.InferType<typeof schema>

// string

required()

Также убирает undefined, но дополнительно валидирует значение.

const schema = yup.string().required()

Разница:

Метод Удаляет undefined Runtime validation
defined Да Нет
required Да Да

Типизация default()

default() влияет на итоговый тип.

const schema = yup.object({
  active: yup.boolean().default(false),
})

type Data = yup.InferType<typeof schema>

Результат:

{
  active: boolean
}

Без default() поле могло бы быть optional.


Типизация mixed()

mixed() используется для универсальных значений.

const schema = yup.mixed<string>()

Тип:

string | undefined

Типизация enum

Через oneOf

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"
}

Типизация union-like структур

Yup ограниченно поддерживает union.

Пример:

const schema = yup.object({
  type: yup.string().oneOf(['email', 'sms']).required(),
})

Но полноценный discriminated union реализовать сложно.

Для сложных union-типов чаще используют:

  • Zod
  • Valibot
  • ArkType

Типизация lazy()

lazy() позволяет строить динамические схемы.

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

Typed yupResolver

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),
})

Ошибки типизации resolver

Несоответствие формы и схемы

type FormData = {
  email: string
  password: string
}

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

Ошибка:

resolver: yupResolver(schema)

Причина — схема не соответствует типу формы.


Строгая типизация errors

После типизации формы ошибки становятся безопасными.

formState.errors.email?.message

TypeScript знает:

  • какие поля существуют
  • какие поля вложенные
  • какие массивы допустимы

Типизация FieldArray

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 mode

Метод strict() отключает преобразование типов.

const schema = yup.number().strict()

Без strict:

schema.validateSync("123")

Результат:

123

Со strict:

ValidationError

На типизацию TypeScript это не влияет, но влияет на runtime-поведение.


stripUnknown и типизация

const schema = yup.object({
  name: yup.string().required(),
})
schema.validate(data, {
  stripUnknown: true,
})

Лишние поля будут удалены runtime-валидатором, однако TypeScript всё равно ориентируется только на схему.


noUnknown

const schema = yup.object({
  name: yup.string(),
}).noUnknown()

Запрещает неизвестные поля.

Полезно для:

  • API payload
  • DTO
  • form submit
  • backend validation

Типизация cast()

cast() преобразует данные в соответствии со схемой.

const schema = yup.object({
  age: yup.number().required(),
})

const result = schema.cast({
  age: '42',
})

Тип результата:

{
  age: number
}

Asserts

Альтернатива 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>
  )
}

Основные рекомендации

Использовать InferType как основной способ вывода типов

type FormData = yup.InferType<typeof schema>

Передавать generic в useForm

useForm<FormData>()

Не дублировать интерфейсы вручную

Плохой подход:

interface FormData {
  email: string
}

при наличии схемы:

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

Лучше:

type FormData = yup.InferType<typeof schema>

Использовать ObjectSchema для строгого соответствия интерфейсам

const schema: yup.ObjectSchema<User>

Осторожно использовать lazy и mixed

Они ухудшают вывод типов и часто требуют ручной типизации.


Отделять runtime validation от compile-time типизации

Yup обеспечивает:

  • runtime validation
  • runtime transformation

TypeScript обеспечивает:

  • compile-time safety
  • autocomplete
  • type inference

Эти механизмы работают совместно, но независимо друг от друга.