Расширение базовых типов Yup

Библиотека Yup построена вокруг набора базовых схем (string, number, boolean, date, array, object, mixed), каждая из которых представляет собой цепочку методов валидации. Архитектура позволяет не только комбинировать встроенные правила, но и расширять поведение типов, добавляя новые методы, трансформации и пользовательские валидаторы.

Расширение базовых типов применяется в случаях, когда стандартного набора проверок недостаточно: требуется корпоративная бизнес-логика, единые правила валидации для проекта или повторно используемые доменные ограничения.


Механизм расширения через addMethod

Основной способ расширения типов в Yup — метод addMethod. Он позволяет добавлять новые методы к существующим схемам.

Общая структура

import * as Yup from 'yup';

Yup.addMethod(Yup.string, 'startsWithUppercase', function (message) {
  return this.test('starts-with-uppercase', message, function (value) {
    if (!value) return true;
    return value[0] === value[0].toUpperCase();
  });
});

После этого метод становится частью цепочки:

const schema = Yup.string().startsWithUppercase('Должно начинаться с заглавной буквы');

Принцип работы addMethod

  • расширяет прототип конкретного типа схемы;
  • возвращает новую схему (immutable-подход);
  • позволяет внедрять кастомные test без дублирования кода;
  • сохраняет цепочечный стиль API.

Расширение конкретных типов схем

Расширение строковых схем

Строки — наиболее частый кандидат на расширение: форматирование, маски, корпоративные правила.

Yup.addMethod(Yup.string, 'noSpaces', function (message) {
  return this.test('no-spaces', message, (value) =>
    value ? !/\s/.test(value) : true
  );
});

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

Yup.string().noSpaces('Пробелы недопустимы');

Дополнительные примеры расширений:

  • проверка доменных email-ограничений;
  • валидация slug-формата;
  • контроль допустимых символов.

Расширение числовых схем

Числовые типы часто расширяются бизнес-ограничениями:

Yup.addMethod(Yup.number, 'isMultipleOf', function (factor, message) {
  return this.test('multiple-of', message, function (value) {
    if (value == null) return true;
    return value % factor === 0;
  });
});

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

Yup.number().isMultipleOf(5, 'Число должно быть кратно 5');

Типичные расширения:

  • ограничения по финансовым округлениям;
  • диапазоны с динамическими шагами;
  • проверка идентификаторов.

Расширение дат

Дата требует специфической логики, особенно в доменных моделях:

Yup.addMethod(Yup.date, 'isWeekday', function (message) {
  return this.test('is-weekday', message, function (value) {
    if (!value) return true;
    const day = value.getDay();
    return day !== 0 && day !== 6;
  });
});

Расширение массивов

Массивы расширяются для контроля структуры:

Yup.addMethod(Yup.array, 'minUniqueItems', function (message) {
  return this.test('min-unique-items', message, function (value) {
    if (!value) return true;
    const unique = new Set(value);
    return unique.size === value.length;
  });
});

Пользовательские валидаторы через test

Базовый механизм расширения в Yup — метод test. Он используется внутри addMethod, но также может применяться напрямую.

Yup.string().test(
  'no-admin',
  'Недопустимое значение',
  (value) => value !== 'admin'
);

Контекст функции test

Функция-валидатор получает объект контекста:

  • value — текущее значение;
  • path — путь в объекте;
  • createError — генерация ошибки;
  • parent — родительский объект;
  • options — параметры схемы.

Пример с динамической ошибкой:

Yup.string().test(
  'min-length-dynamic',
  function (value) {
    const min = this.options.context?.minLength || 0;

    return value && value.length >= min
      ? true
      : this.createError({ message: `Минимум ${min} символов` });
  }
);

Расширение mixed как основа универсальных схем

Тип mixed является базовым уровнем абстракции, от которого наследуются все остальные схемы. Его расширение позволяет создавать универсальные валидаторы.

Yup.addMethod(Yup.mixed, 'isRequiredIf', function (condition, message) {
  return this.test('is-required-if', message, function (value) {
    const shouldBeRequired = condition(this.parent);
    if (!shouldBeRequired) return true;
    return value != null;
  });
});

Применение:

Yup.object({
  email: Yup.string().email(),
  phone: Yup.string().isRequiredIf(
    (parent) => !parent.email,
    'Телефон обязателен, если нет email'
  )
});

Композиция расширений и повторное использование

Расширения часто комбинируются в наборы правил:

const commonStringRules = (schema) =>
  schema.trim().min(3).max(50);

const usernameSchema = commonStringRules(
  Yup.string()
).noSpaces().startsWithUppercase();

Такой подход позволяет:

  • централизовать бизнес-правила;
  • избегать дублирования;
  • поддерживать единый стиль валидации.

Динамическая логика через when

Хотя when не является расширением, он часто используется вместе с кастомными методами:

Yup.string().when('role', {
  is: 'admin',
  then: (schema) => schema.required().min(10),
  otherwise: (schema) => schema.min(3)
});

В связке с расширенными методами:

Yup.string()
  .noSpaces()
  .when('strict', {
    is: true,
    then: (schema) => schema.startsWithUppercase()
  });

Трансформации значений при расширении

Метод transform позволяет изменять входные данные до валидации:

Yup.addMethod(Yup.string, 'normalizeSpaces', function () {
  return this.transform((value) =>
    typeof value === 'string' ? value.replace(/\s+/g, ' ') : value
  );
});

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

Yup.string().normalizeSpaces().trim();

Локализация ошибок

Расширения часто сопровождаются настройкой сообщений:

Yup.setLocale({
  mixed: {
    required: 'Поле обязательно'
  },
  string: {
    min: 'Слишком короткое значение'
  }
});

При кастомных методах важно передавать сообщения через параметры test, а не хардкодить их внутри логики.


Типизация расширений в TypeScript

При использовании TypeScript необходимо расширять интерфейсы схем:

import 'yup';

declare module 'yup' {
  interface StringSchema {
    noSpaces(message?: string): this;
    startsWithUppercase(message?: string): this;
  }
}

Без этого расширенные методы будут недоступны в типах, несмотря на их наличие в runtime.


Паттерны создания расширяемых схем

Фабрики схем

const createEmailSchema = () =>
  Yup.string()
    .email()
    .required()
    .trim();

Базовый слой + расширения

const baseString = Yup.string().trim().min(2);

const username = baseString.noSpaces().startsWithUppercase();

Переиспользуемые доменные правила

const isPositiveId = Yup.number().integer().positive();
const isExternalId = isPositiveId.isMultipleOf(3, 'Некорректный ID');

Расширение через кастомные object-схемы

Объекты позволяют внедрять сложную структуру:

Yup.addMethod(Yup.object, 'withTimestamps', function () {
  return this.shape({
    createdAt: Yup.date().required(),
    updatedAt: Yup.date().required()
  });
});

Практические особенности архитектуры расширений

  • расширения должны быть идемпотентными;
  • методы не должны мутировать входные схемы;
  • повторное объявление addMethod приводит к перезаписи поведения;
  • сложная логика лучше выносится в отдельные функции, а не внутрь test;
  • композиция предпочтительнее глубокой вложенности условий;
  • расширения должны оставаться предсказуемыми и тестируемыми.