Создание типобезопасных схем

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

Базовая идея типобезопасных схем

Схема валидации в Yup представляет собой объект, описывающий структуру данных и правила проверки. В TypeScript-окружении эта схема может быть источником типов, если используется механизм вывода через InferType.

import * as Yup from "yup";

const userSchema = Yup.object({
  id: Yup.number().required(),
  name: Yup.string().required(),
  email: Yup.string().email().required(),
});

С точки зрения TypeScript, сама по себе схема не гарантирует типобезопасность результата. Для этого используется вывод типа:

type User = Yup.InferType<typeof userSchema>;

Теперь тип User автоматически соответствует структуре схемы.


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

Механизм InferType анализирует структуру схемы и преобразует её в соответствующий TypeScript-тип:

  • Yup.string()string
  • Yup.number()number
  • Yup.boolean()boolean
  • Yup.array(Yup.string())string[]
  • Yup.object({...}) → объект с соответствующими полями

Это позволяет синхронизировать runtime-валидацию и compile-time типы.


Объектные схемы и строгая типизация

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

const productSchema = Yup.object({
  id: Yup.string().required(),
  price: Yup.number().required(),
  description: Yup.string().optional(),
}).noUnknown(true);

Метод noUnknown(true) ограничивает добавление лишних полей, приближая поведение схемы к строгим типам TypeScript.


Явное управление типами через as const и shape

Для более предсказуемого вывода типов используется явное определение структуры:

const schema = Yup.object({
  title: Yup.string().required(),
  count: Yup.number().required(),
} as const);

Такой подход позволяет избежать расширения типов и сохраняет буквальные значения в процессе вывода.


Использование partial и optional в схемах

Типобезопасность зависит от корректного отражения необязательных полей. В Yup это реализуется через optional() и required().

const profileSchema = Yup.object({
  username: Yup.string().required(),
  bio: Yup.string().optional(),
  age: Yup.number().notRequired(),
});

В TypeScript это приводит к следующей структуре:

  • username: string
  • bio?: string
  • age?: number

Вложенные схемы и композиция типов

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

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

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

Типизация автоматически становится вложенной:

type User = {
  name: string;
  address: {
    city: string;
    zip: string;
  };
};

Массивы и типобезопасные коллекции

Схемы массивов позволяют описывать тип элементов и гарантировать однородность структуры.

const tagsSchema = Yup.array(Yup.string().required());

Вывод типа:

string[]

Для объектов:

const usersSchema = Yup.array(
  Yup.object({
    id: Yup.number().required(),
    name: Yup.string().required(),
  })
);

Результат:

Array<{ id: number; name: string }>

Унификация схем через generics-подход

Хотя Yup не использует generics напрямую в API, типобезопасность достигается через обобщённое описание схем и повторное использование типов.

type BaseEntity = {
  id: number;
};

const baseSchema = Yup.object({
  id: Yup.number().required(),
});

Дальнейшие схемы могут расширять базовую структуру:

const postSchema = baseSchema.shape({
  title: Yup.string().required(),
});

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

Для сложных моделей применяется композиция схем через concat.

const a = Yup.object({
  a: Yup.string().required(),
});

const b = Yup.object({
  b: Yup.number().required(),
});

const combined = a.concat(b);

Тип результирующей схемы:

{ a: string; b: number }

Кастомные схемы и типизация mixed

Тип mixed используется для произвольных значений, но при этом теряется часть типобезопасности. Его можно уточнять через кастомные проверки.

const customSchema = Yup.mixed()
  .test("is-date", "Invalid date", (value): value is Date => value instanceof Date);

Такой подход позволяет вручную восстановить контроль типов.


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

Схемы могут изменять входные данные, что влияет на итоговый тип.

const numberFromString = Yup.string().transform((value, originalValue) => {
  return Number(originalValue);
});

Здесь важно учитывать, что итоговый тип становится number, несмотря на исходный string.


Типобезопасная валидация и результат схемы

После валидации результат приводится к типу, выведенному из схемы:

const data: User = await userSchema.validate(input);

Если схема и типы синхронизированы, дальнейшая работа с data становится безопасной без дополнительных проверок.


Расширение схем без потери типизации

При модификации схем важно сохранять соответствие типов. Расширение через shape позволяет добавлять новые поля без разрушения структуры:

const extendedUser = userSchema.shape({
  role: Yup.string().required(),
});

Результирующий тип автоматически включает новое поле.


Ограничения типобезопасности Yup

Несмотря на возможности TypeScript-интеграции, типобезопасность в Yup имеет ограничения:

  • отсутствие строгих generics в runtime API
  • возможные расхождения между трансформациями и типами
  • ограниченная поддержка дискриминируемых union-типов

Поэтому типы рассматриваются как производные от схемы, а не как первичный источник истины.


Согласование схем и доменных типов

В сложных приложениях схема становится отражением доменной модели. Поддержание синхронизации достигается через единый источник описания:

const orderSchema = Yup.object({
  id: Yup.string().required(),
  total: Yup.number().required(),
  status: Yup.mixed<"pending" | "paid" | "cancelled">().required(),
});

Тип выводится напрямую:

type Order = Yup.InferType<typeof orderSchema>;

Такой подход уменьшает дублирование и снижает риск рассинхронизации логики и типов.