Порядок выполнения трансформаций

Валидация в Yup состоит не только из проверок. Перед запуском правил библиотека проходит через цепочку преобразований данных — трансформаций (transform). Именно этот этап определяет, какое значение попадёт в валидаторы (required, min, matches, test и другие).

Полный порядок обработки выглядит так:

  1. Получение исходного значения
  2. Применение встроенных трансформаций типа
  3. Выполнение пользовательских transform
  4. Применение default
  5. Выполнение проверок (tests)
  6. Возврат итогового значения

Ключевая особенность Yup — проверки работают уже над преобразованным значением, а не над оригинальным вводом.


Базовый порядок выполнения

Пример:

import * as yup from 'yup';

const schema = yup
  .string()
  .trim()
  .lowercase()
  .min(3);

schema.validate('   HELLO   ');

Порядок будет следующим:

1. Исходное значение:
   "   HELLO   "

2. trim():
   "HELLO"

3. lowercase():
   "hello"

4. min(3):
   проверка проходит

Результат:

"hello"

Встроенные трансформации типов

Каждый тип Yup имеет собственные встроенные преобразования.

string()

yup.string()

Автоматически приводит значение к строке.

await yup.string().validate(123);

Результат:

"123"

number()

await yup.number().validate("42");

Результат:

42

Yup пытается выполнить:

Number(value)

Если преобразование невозможно:

await yup.number().validate("abc");

Возникнет ошибка:

this must be a `number` type

boolean()

await yup.boolean().validate("true");

Результат:

true

Поддерживаются значения:

"true"
"false"
"1"
"0"
1
0

date()

await yup.date().validate("2025-01-01");

Результат:

Date

Yup вызывает:

new Date(value)

Когда выполняется transform()

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

Пример:

const schema = yup
  .number()
  .transform(value => value * 2);

await schema.validate("5");

Порядок:

1. "5"
2. number() → 5
3. transform() → 10

Результат:

10

Сигнатура transform()

Метод принимает функцию:

transform((currentValue, originalValue) => {})

currentValue

Значение после предыдущих трансформаций.

originalValue

Исходное значение до любых преобразований.


Разница между currentValue и originalValue

const schema = yup
  .number()
  .transform((value, originalValue) => {
    console.log(value);
    console.log(originalValue);

    return value;
  });

await schema.validate("25");

Вывод:

25
"25"

Цепочка transform()

Трансформации выполняются сверху вниз.

const schema = yup
  .string()
  .transform(value => value.trim())
  .transform(value => value.toUpperCase())
  .transform(value => `[${value}]`);

await schema.validate(" hello ");

Порядок:

1. " hello "
2. "hello"
3. "HELLO"
4. "[HELLO]"

Результат:

"[HELLO]"

Встроенные методы как transform()

Методы:

  • trim()
  • lowercase()
  • uppercase()
  • camelCase()
  • ensure()
  • compact()

являются обычными трансформациями внутри цепочки.

Пример:

const schema = yup
  .string()
  .trim()
  .uppercase();

await schema.validate(" test ");

Результат:

"TEST"

Как transform() влияет на validation tests

Проверки получают уже изменённое значение.

const schema = yup
  .string()
  .trim()
  .min(5);

await schema.validate("   hello   ");

Порядок:

1. trim() → "hello"
2. min(5)

Без trim() длина была бы 11 символов.


transform() и required()

Трансформации выполняются раньше required().

const schema = yup
  .string()
  .transform(value => value === '' ? undefined : value)
  .required();

Проверка:

await schema.validate('');

Порядок:

1. "" → undefined
2. required() → ошибка

Использование transform() для очистки данных

Удаление лишних символов

const phoneSchema = yup
  .string()
  .transform(value => value.replace(/\D/g, ''));
await phoneSchema.validate('+7 (777) 123-45-67');

Результат:

"77771234567"

Нормализация email

const schema = yup
  .string()
  .trim()
  .lowercase()
  .email();
await schema.validate('  ADMIN@SITE.COM ');

Результат:

"admin@site.com"

Возврат undefined внутри transform()

Это важный механизм управления логикой валидации.

const schema = yup
  .number()
  .transform(value => isNaN(value) ? undefined : value);

Теперь:

await schema.validate('abc');

приведёт к:

undefined

Далее поведение зависит от:

  • required()
  • nullable()
  • optional()

Порядок transform() и default()

default() применяется после трансформаций.

const schema = yup
  .string()
  .transform(value => value || undefined)
  .default('guest');

Проверка:

schema.cast('');

Порядок:

1. "" → undefined
2. default() → "guest"

Результат:

"guest"

cast() и transform()

Метод cast() запускает трансформации без выполнения validation tests.

const schema = yup
  .string()
  .trim()
  .lowercase();

schema.cast(' HELLO ');

Результат:

"hello"

Но:

schema.cast(123);

тоже выполнит преобразование:

"123"

validate() против cast()

cast()

Только преобразование:

transform → result

validate()

Полный цикл:

transform → tests → result/error

strict() отключает transform()

Режим strict() запрещает автоматические преобразования.

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

await schema.validate("42");

Ошибка:

this must be a `number` type

Без strict() строка была бы преобразована в число.


strict() и пользовательские transform()

В строгом режиме не работают даже пользовательские трансформации.

const schema = yup
  .string()
  .transform(value => value.trim())
  .strict();

await schema.validate(' hello ');

Результат:

" hello "

Трансформация пропущена.


nullable() и порядок обработки

const schema = yup
  .string()
  .nullable()
  .transform(value => value === '' ? null : value);

Проверка:

await schema.validate('');

Порядок:

1. "" → null
2. nullable() разрешает null
3. validation проходит

transform() в объектах

Трансформации выполняются отдельно для каждого поля.

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

Проверка:

await schema.validate({
  name: ' Alex ',
  age: '25'
});

Результат:

{
  name: 'Alex',
  age: 25
}

Порядок обработки объекта

Для объекта Yup:

  1. Преобразует объект целиком
  2. Обрабатывает каждое поле
  3. Выполняет проверки полей
  4. Выполняет проверки объекта

transform() внутри array()

const schema = yup.array(
  yup.string().trim().uppercase()
);

await schema.validate([
  ' hello ',
  ' world '
]);

Результат:

['HELLO', 'WORLD']

array().compact() как transform()

const schema = yup
  .array()
  .compact();

Удаляются:

false
null
0
""
undefined

Пример:

await schema.validate([
  'A',
  '',
  'B',
  null
]);

Результат:

['A', 'B']

Ленивые схемы и порядок выполнения

lazy() создаёт схему динамически после получения значения.

const schema = yup.lazy(value => {
  if (typeof value === 'string') {
    return yup.string().trim();
  }

  return yup.number();
});

Порядок:

1. Получение значения
2. Выбор схемы
3. Выполнение transform()
4. Валидация

transform() и custom test()

const schema = yup
  .string()
  .trim()
  .test(
    'is-admin',
    'Not admin',
    value => value === 'admin'
  );

Проверка:

await schema.validate('   admin   ');

test() получает:

"admin"

Почему transform() не должен содержать validation logic

Плохой пример:

.transform(value => {
  if (value.length < 5) {
    throw new Error();
  }

  return value;
})

Трансформации должны:

  • изменять данные
  • нормализовать формат
  • подготавливать значения

Проверки должны выполняться через:

  • min
  • max
  • matches
  • test

Идемпотентность transform()

Хорошая трансформация должна давать одинаковый результат при повторном вызове.

Хорошо:

value => value.trim()

Плохо:

value => value + Math.random()

Ошибки внутри transform()

Если внутри transform возникает исключение:

.transform(() => {
  throw new Error('Fail');
})

валидация завершается аварийно.

Лучше возвращать безопасные значения:

.transform(value => {
  try {
    return JSON.parse(value);
  } catch {
    return undefined;
  }
})

Асинхронные transform() не поддерживаются

Нельзя:

.transform(async value => {})

transform() всегда синхронный.

Асинхронность допускается только внутри test().


Порядок нескольких test()

После завершения всех transform Yup запускает проверки.

const schema = yup
  .string()
  .trim()
  .test('a', 'A', value => true)
  .test('b', 'B', value => true);

Порядок:

1. transform
2. test a
3. test b

Что происходит при abortEarly

validate(value, {
  abortEarly: true
});

Даже при ранней остановке:

  • все transform выполняются полностью
  • прерываются только validation tests

Взаимодействие transform() и when()

const schema = yup.object({
  type: yup.string(),

  value: yup.string().when('type', {
    is: 'email',

    then: schema =>
      schema
        .trim()
        .lowercase()
        .email()
  })
});

Порядок:

1. Определение условия when()
2. Построение схемы
3. transform()
4. validation tests

Полный жизненный цикл значения

Пример сложной схемы:

const schema = yup
  .string()
  .transform(value => value.trim())
  .transform(value => value.toLowerCase())
  .transform(value => value || undefined)
  .default('guest')
  .required()
  .min(3);

Проверка:

await schema.validate('   ADMIN   ');

Полный порядок:

1. Исходное значение:
   "   ADMIN   "

2. trim():
   "ADMIN"

3. toLowerCase():
   "admin"

4. value || undefined:
   "admin"

5. default():
   не применяется

6. required():
   проходит

7. min(3):
   проходит

8. Финальный результат:
   "admin"

Внутренний принцип pipeline

Yup строит конвейер обработки:

input
  ↓
type casting
  ↓
transform chain
  ↓
default values
  ↓
validation tests
  ↓
output

Понимание этой последовательности критически важно для:

  • очистки пользовательского ввода
  • нормализации API-данных
  • построения сложных форм
  • обработки nullable-полей
  • динамической валидации
  • предотвращения скрытых ошибок типов
  • проектирования предсказуемых схем данных