Валидация файловых полей в веб-приложениях требует учёта специфики
объекта File, который не является примитивом и не
подчиняется стандартным правилам проверки строк или чисел. При работе с
формами на основе Yup и резолвером YupResolver
используется тип mixed, позволяющий описывать произвольные
структуры данных, включая файлы, загружаемые пользователем.
Ключевая особенность проверки типа файла заключается в том, что корректность определяется не только расширением, но и MIME-типом, который предоставляется браузером. В реальных сценариях оба параметра могут быть искажены или отсутствовать, поэтому надёжная схема валидации должна учитывать несколько уровней проверки.
Типичная схема начинается с определения поля как mixed,
поскольку Yup не предоставляет специализированного типа для
файловых объектов:
import * as Yup from 'yup';
const fileSchema = Yup.object({
file: Yup.mixed()
});
На этом этапе поле не содержит ограничений. Следующий шаг — добавление обязательности и первичной проверки наличия файла:
file: Yup.mixed()
.required('Файл обязателен')
Однако такой проверки недостаточно, поскольку объект может существовать, но не являться допустимым файлом.
Первичная защита заключается в проверке структуры объекта:
file: Yup.mixed()
.required('Файл обязателен')
.test('is-file', 'Некорректный объект файла', (value) => {
return value && value instanceof File;
})
Использование instanceof File работает в браузерной
среде, но может быть недостаточным при серверном рендеринге или
тестировании, где объект может иметь иную природу. Поэтому дополнительно
проверяются свойства:
namesizetypeОдним из основных способов контроля типа файла является проверка MIME-строки:
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
file: Yup.mixed()
.required('Файл обязателен')
.test('file-type', 'Неподдерживаемый тип файла', (value) => {
if (!value) return false;
return allowedTypes.includes(value.type);
})
MIME-тип является более надёжным признаком, чем расширение, однако не гарантирует абсолютную точность, поскольку может быть подменён.
Дополнительный уровень контроля — анализ имени файла:
const allowedExtensions = ['jpg', 'jpeg', 'png', 'pdf'];
function getExtension(fileName) {
return fileName.split('.').pop().toLowerCase();
}
file: Yup.mixed()
.required('Файл обязателен')
.test('file-extension', 'Недопустимое расширение', (value) => {
if (!value || !value.name) return false;
const ext = getExtension(value.name);
return allowedExtensions.includes(ext);
})
Комбинация MIME-типа и расширения снижает вероятность некорректной загрузки.
Практическая реализация обычно объединяет несколько проверок в одном тесте:
const schema = Yup.object({
file: Yup.mixed()
.required('Файл обязателен')
.test('file-validation', 'Файл не соответствует требованиям', (value) => {
if (!value) return false;
const isFile = value instanceof File;
if (!isFile) return false;
const allowedMime = ['image/jpeg', 'image/png', 'application/pdf'];
const allowedExt = ['jpg', 'jpeg', 'png', 'pdf'];
const hasValidType = allowedMime.includes(value.type);
const extension = value.name?.split('.').pop().toLowerCase();
const hasValidExtension = allowedExt.includes(extension);
return hasValidType && hasValidExtension;
})
});
Такой подход позволяет учитывать реальные ограничения браузера и одновременно защищает от некорректных данных.
В связке с формами на основе react-hook-form схема
передаётся в YupResolver, который обеспечивает
синхронизацию правил валидации с состоянием формы:
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
const form = useForm({
resolver: yupResolver(schema)
});
При этом YupResolver автоматически обрабатывает
результат схемы и возвращает ошибки в формате, совместимом с системой
управления формами.
В случае множественной загрузки используется массив объектов
File, и схема усложняется проверкой каждого элемента:
file: Yup.array()
.of(
Yup.mixed()
.test('is-file', 'Некорректный файл', (value) => value instanceof File)
.test('file-type', 'Недопустимый тип', (value) =>
['image/jpeg', 'image/png'].includes(value.type)
)
)
Здесь важно учитывать, что валидируется каждый элемент массива отдельно, а общая ошибка может возникнуть при первом несоответствии.
Проверка типа файла часто дополняется контролем размера, что повышает устойчивость загрузки:
const MAX_SIZE = 5 * 1024 * 1024;
file: Yup.mixed()
.test('file-size', 'Файл слишком большой', (value) => {
if (!value) return false;
return value.size <= MAX_SIZE;
})
Размер проверяется в байтах, что позволяет точно контролировать допустимые пределы.
Наиболее практичный вариант объединяет тип, расширение и размер в одной структуре проверки:
const schema = Yup.object({
file: Yup.mixed()
.required('Файл обязателен')
.test('file-check', 'Файл не соответствует требованиям', (file) => {
if (!file || !(file instanceof File)) return false;
const allowedTypes = ['image/jpeg', 'image/png'];
const allowedExtensions = ['jpg', 'jpeg', 'png'];
const maxSize = 3 * 1024 * 1024;
const extension = file.name.split('.').pop().toLowerCase();
return (
allowedTypes.includes(file.type) &&
allowedExtensions.includes(extension) &&
file.size <= maxSize
);
})
});
При использовании резолвера ошибки, возвращаемые схемой,
преобразуются в структуру fieldErrors, где каждая ошибка
связывается с конкретным полем формы. Для файловых полей это особенно
важно, поскольку ошибка может возникнуть не только из-за отсутствия
значения, но и из-за несоответствия типа, размера или структуры
объекта.
Приоритетность проверок внутри Yup влияет на то, какая
ошибка будет возвращена первой, поскольку выполнение test
прекращается при первом false, если не задано иное
поведение через abortEarly.
При использовании TypeScript типизация файлового поля требует явного описания:
interface FormValues {
file: File | null;
}
Схема при этом остаётся на уровне mixed, но логическая
связка типов помогает избежать несоответствий между формой и
валидацией.
const schema: Yup.Schema<FormValues> = Yup.object({
file: Yup.mixed<File>()
});
В некоторых сценариях файл может приходить не как экземпляр
File, а как объект с аналогичной структурой (например,
после десериализации). В таких случаях проверка должна опираться на
наличие ключевых полей:
const isFileLike = (obj) =>
obj &&
typeof obj === 'object' &&
typeof obj.name === 'string' &&
typeof obj.size === 'number' &&
typeof obj.type === 'string';
Такой подход позволяет расширить совместимость схемы без привязки к браузерной реализации.