Использование InferType

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

Основная идея заключается в том, что схема Yup становится единственным источником истины, а TypeScript тип выводится непосредственно из неё.


Базовое использование Yup.InferType

Тип InferType применяется к схеме следующим образом:

import * as Yup from 'yup';

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

Из этой схемы можно вывести тип:

type User = Yup.InferType<typeof userSchema>;

В результате User будет эквивалентен:

type User = {
  id: number;
  name: string;
  email: string;
};

Таким образом, любое изменение схемы автоматически отражается в типе данных.


Примитивные типы и их соответствие

InferType корректно отображает все базовые типы Yup:

  • Yup.string()string
  • Yup.number()number
  • Yup.boolean()boolean
  • Yup.date()Date

Пример:

const schema = Yup.object({
  title: Yup.string(),
  views: Yup.number(),
  published: Yup.boolean(),
  createdAt: Yup.date(),
});

type Post = Yup.InferType<typeof schema>;

Результат:

type Post = {
  title?: string;
  views?: number;
  published?: boolean;
  createdAt?: Date;
};

Обязательные и необязательные поля

Yup по умолчанию делает поля необязательными, если не указан .required().

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

Тип будет:

type Result = {
  title: string;
  description?: string | undefined;
};

Важно, что InferType сохраняет информацию о возможности undefined, что позволяет корректно работать с проверками на уровне TypeScript.


Работа с вложенными объектами

InferType полноценно поддерживает вложенные структуры:

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

Тип:

type Result = {
  user?: {
    id: number;
    profile?: {
      firstName?: string;
      lastName?: string;
    };
  };
};

Вложенность сохраняется рекурсивно, включая все уровни схемы.


Массивы и InferType

Для массивов используется Yup.array():

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

Тип:

type Result = {
  tags?: string[];
};

Если внутри массива сложный объект:

const schema = Yup.object({
  comments: Yup.array().of(
    Yup.object({
      id: Yup.number(),
      text: Yup.string(),
    })
  ),
});

Результат:

type Result = {
  comments?: {
    id?: number;
    text?: string;
  }[];
};

Объединение типов и oneOf, mixed

При использовании mixed или ограничений через oneOf типизация становится более узкой:

const schema = Yup.object({
  status: Yup.mixed<'draft' | 'published' | 'archived'>().oneOf([
    'draft',
    'published',
    'archived',
  ]),
});

Тип:

type Result = {
  status?: 'draft' | 'published' | 'archived';
};

Это позволяет использовать Yup как источник строго типизированных union-значений.


nullable и влияние на тип

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

const schema = Yup.object({
  name: Yup.string().nullable(),
});

Тип:

type Result = {
  name?: string | null;
};

Это важно учитывать при работе с API, где отсутствие значения и null различаются.


default() и влияние на InferType

Если задано значение по умолчанию:

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

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

type Result = {
  role?: string;
};

Важно: InferType не подставляет runtime-дефолты в тип как обязательные значения, так как они применяются только при валидации.


Ограничения InferType

Несмотря на мощность, существуют ограничения:

1. Потеря части runtime-логики

Некоторые трансформации не отражаются в типе:

Yup.string().transform((value) => value.trim())

TypeScript не учитывает .transform().


2. Условные схемы (when)

При использовании .when() тип становится объединённым и может терять точность:

const schema = Yup.object({
  isAdmin: Yup.boolean(),
  role: Yup.string().when('isAdmin', {
    is: true,
    then: (s) => s.required(),
    otherwise: (s) => s.optional(),
  }),
});

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


3. Mixed и кастомные валидаторы

При использовании Yup.mixed() с кастомной логикой итоговый тип часто требует явного уточнения через дженерики.


Использование с дженериками

Можно явно задавать тип, если требуется усилить контроль:

const schema: Yup.ObjectSchema<{ id: number }> = Yup.object({
  id: Yup.number().required(),
});

type Result = Yup.InferType<typeof schema>;

В этом случае тип будет строго согласован с дженериком схемы.


Комбинирование нескольких схем

При объединении схем через concat тип также объединяется:

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

const extended = base.concat(
  Yup.object({
    name: Yup.string(),
  })
);

type Result = Yup.InferType<typeof extended>;

Результат:

type Result = {
  id?: number;
  name?: string;
};

Практическая роль InferType в архитектуре

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

  • формами ввода
  • API-слоем
  • бизнес-логикой
  • клиентской валидацией

Схема становится единым контрактом данных, а TypeScript — её статическим отражением.