Решаем задачу: как сохранить нервные клетки пользователей с помощью валидации поля ввода
Всем привет! Меня зовут Алексей Гмитрон, я фулстек-разработчик и наставник на курсе «Фронтенд-разработчик» в Практикуме. Довольно долгое время я разрабатываю интерфейсы, а ещё дольше — пользуюсь ими.
В этом году я много путешествовал, поэтому нередко заполнял формы с анкетами на разные визы — в них бывало по 30—40 полей. Когда что-то шло не так, часто сайты не давали никакой обратной связи. Иногда они сбрасывали всё, что я заполнял в течение часа, если одно из полей невалидно.
Решить проблему могла бы валидация. Это критически важная часть разработки веб-приложений, которая соотносит данные с необходимым форматом и указывает на ошибки. Также она гарантирует безопасность дальнейшей обработки этих данных.
В этой статье мы разберёмся, как настроить валидацию поля ввода.
Задача, которую мы будем решать
Есть некий интернет-магазин, где пользователи должны указать свой адрес, чтобы оформить заказ. Они случайно вводят некорректные или неполные данные, потому что у сайта отсутствует валидация поля для ввода почтового индекса.
Дело не в том, что индекс некорректен (например, указан чей-то чужой). Он невалиден, то есть не соответствует необходимому формату, и даже нельзя установить, чей он.
В зависимости от выбранной страны индекс может иметь разный формат — давайте рассмотрим несколько примеров.
Правильные (валидные) почтовые индексы:
США: 10001 (Нью-Йорк), 90210 (Беверли-Хиллз) — состоят из 5 цифр.
Россия: 101000 (Москва), 190000 (Санкт-Петербург) — из 6 цифр.
Великобритания: SW1A 1AA (Букингемский дворец, Лондон), M1 1AA (Манчестер) — из букв и цифр в определённой последовательности.
Неправильные (невалидные) почтовые индексы:
США: 1234, AB123 — первый слишком короткий и не соответствует стандарту из 5 цифр, а второй содержит буквы, что недопустимо для американских индексов.
Россия: 1234567, 12AB56 — первый слишком длинный, второй содержит буквы.
Великобритания: A1 1AA, 123 AB — первый нарушает стандартное строение британских индексов, второй не соответствует общепринятой структуре.
К чему приводит отсутствие валидации: заказы не могут быть обработаны из-за некорректных адресов, что ведёт к задержкам в доставке и недовольству клиентов. Пользователи не получают никакой обратной связи о проблеме в момент оформления заказа, что ведёт к путанице и разочарованию.
Отдел обработки заказов перегружен запросами на коррекцию адресов. Это увеличивает нагрузку на сотрудников и повышает операционные расходы. Некоторые клиенты могут отказаться от повторного заказа из-за плохого опыта взаимодействия с сайтом — это ведёт к снижению выручки.
Необходимо внедрить эффективную валидацию почтового индекса на стороне клиента и сервера. На фронтенде валидация значительно улучшит пользовательский опыт за счёт предоставления мгновенной обратной связи. На бэкенде она критически важна для обеспечения безопасности, целостности и надёжности обработки данных.
Почему TypeScript недостаточно для валидации
TypeScript проверяет типы данных и их соответствие во время компиляции кода. Это означает, что он анализирует ваш код и предупреждает о возможных ошибках типизации до того, как код будет запущен. Это полезно для предотвращения множества распространённых ошибок в JavaScript, таких как передача строки туда, где ожидается число, или обращение к свойствам, которых не существует в объекте.
Однако, когда код скомпилирован и запущен (runtime), TypeScript уже не может контролировать, какие данные передаются в функции или присваиваются переменным. Например, если функция ожидает число, TypeScript гарантирует, что код не будет компилироваться, если вы передадите в неё строку. Но он не может проверить, действительно ли число находится в ожидаемом диапазоне (например, положительное).
Кроме того, данные, получаемые от пользователя через формы, от внешних API или из баз данных, могут быть непредсказуемыми и не соответствовать ожидаемым типам, определённым в TypeScript.
Таким образом, TypeScript является отличным инструментом для обеспечения типобезопасности на этапе разработки, но он не заменяет необходимость валидации данных во время выполнения программы. Всегда необходимо проверять корректность и безопасность входящих данных во время выполнения кода (runtime).
Дальше мы рассмотрим несколько вариантов, как это можно сделать.
Рабочий, но не лучший вариант: реализация кода «в лоб»
Допустим, мы делаем форму для регистрации пользователя. Давайте определим, какие могут быть поля, и подумаем, что считается валидным для этих полей:
имя пользователя — не пустая строка;
возраст — обязательно число, строка вида «Мне 16» не подойдёт;
электронная почта — формат something@domain.zone;
пароль — не менее 8 символов, содержит минимум одну цифру, одну заглавную и одну строчную букву
Наша задача не такая сложная: нужно в то время, когда пользователь набирает текст, проверять все эти поля на соответствие заданному формату. Напишем для этого функцию:
function validateUserData(userData) {
const errors = [];
// Проверка имени
if (typeof userData.name !== 'string' || userData.name.trim() === '') {
errors.push('Имя не указано или некорректно');
}
// Проверка возраста
if (typeof userData.age !== 'number' || userData.age < 18) {
errors.push('Возраст должен быть числом не меньше 18');
}
// Проверка электронной почты
if (typeof userData.email !== 'string' || !userData.email.includes('@')) {
errors.push('Некорректный формат электронной почты');
}
// Проверка пароля
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/;
if (typeof userData.password !== 'string' || !passwordRegex.test(userData.password)) {
errors.push('Пароль должен быть не менее 8 символов и содержать минимум одну цифру, одну заглавную и одну строчную букву');
}
return errors;
}
// Пример использования
const userData = {
name: 'Иван',
age: 20,
email: 'ivan@example.com',
password: 'Password123'
};
const validationErrors = validateUserData(userData);
if (validationErrors.length > 0) {
console.log('Ошибки валидации:', validationErrors);
} else {
console.log('Все данные корректны');
}
В этом примере функция validateUserData
принимает объект userData
и возвращает массив с сообщениями об ошибках. Для каждого поля выполняется серия if-else
проверок, чтобы убедиться, что данные соответствуют определённым критериям.
Этот подход, хоть и работает, имеет несколько недостатков:
громоздкость — большое количество
if-else
усложняет чтение и понимание кода;
трудность поддержки — добавление новых правил валидации или изменение существующих может потребовать значительных изменений в коде;
повторяемость — аналогичные проверки могут потребоваться в разных частях приложения, что приводит к дублированию кода.
Более удачный вариант: декларативный подход к валидации
Если посмотреть на код выше, скорее всего, возникнет мысль, что было бы круто описать тип в виде JS-объекта (для тех, кто знаком с TypeScript, может подойти в качестве примера интерфейс или структурный тип). Вот бы написать что-то такое:
// Было бы классно иметь что-то такое в TypeScript
interface User {
name: string;
age: number;
email: Email;
password: Password;
}
// или что-нибудь такое в чистом JavaScript
const User = {
name: String,
age: Number,
email: Email,
password: Password
}
И жизнь была бы прекрасна без этих всех if-else…
К счастью, уже есть проверенное решение — библиотека Zod.
Zod — это библиотека валидации для JavaScript и TypeScript, предназначенная для создания схем и проверки данных. Она примечательна своей гибкостью и мощностью, позволяя разработчикам строго определять структуры данных и правила, которым эти данные должны соответствовать. Кроме того, Zod отлично выводит типы, что делает это мощнейшим инструментом в сочетании с TypeScript.
Основные особенности Zod:
типобезопасность — Zod эффективно работает с TypeScript, позволяя автоматически генерировать типы из ваших схем. Это обеспечивает дополнительный уровень безопасности при работе с данными, гарантируя, что они соответствуют ожидаемым структурам;
гибкость и расширяемость — библиотека предоставляет широкий набор встроенных валидаторов и позволяет легко создавать кастомные проверки;
простота использования — Zod предлагает чистый и интуитивно понятный API, сокращая количество шаблонного кода.
Давайте перепишем наш пример кода на Zod:
import { z } from 'zod';
const passwordValidationRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/;
const userSchema = z.object({
name: z.string(),
age: z.number().min(18),
email: z.string().email(),
password: z.string()
.min(8, { message: "Пароль должен быть не менее 8 символов" })
.refine((val) => passwordValidationRegex.test(val), {
message: "Пароль должен содержать минимум одну цифру, одну заглавную и одну строчную букву",
}),
});
Здесь создается схема валидации данных пользователя. Схема — это описание типа данных, который определяет, какие данные ожидаются и как они должны быть структурированы. В общем и целом очень похоже на TypeScript, однако он не может нам помочь во время выполнения кода, ведь он испаряется после компиляции и служит нам только в момент разработки.
Наша схема описывает поля следующим образом:
name: z.string()
— поле name должно быть строкой;age: z.number().min(18
) — поле age должно быть числом, минимальное допустимое значение — 18;email: z.string().email()
— поле email должно быть строкой, соответствующей формату электронной почты.
Чуть интереснее валидация пароля: это строка с минимальным количеством символов 8, после которой вызывается функция refine
. Эта функция позволяет задать свои собственные условия или правила, которым должны соответствовать данные. Она не ограничивается стандартными проверками, такими как проверка типа или длины строки. В нашем примере:
password: z.string()
.min(8, { message: "Пароль должен быть не менее 8 символов" })
.refine((val) => passwordValidationRegex.test(val), {
message: "Пароль должен содержать минимум одну цифру, одну заглавную и одну строчную букву",
}),
Метод refine
переопределяет (о чём и говорит название метода) стандартное поведение строки: чтобы определить, как должно валидироваться конкретно это строчное поле, мы объявляем функцию, в которой делается проверка строки на соответствие регулярному выражению. Вместо регулярного выражения могла быть и любая другая функция с иными проверками — главное, чтобы функция возвращала true или false.
Проверить, соответствуют ли действительные данные определённой схеме, тоже очень просто: для этого существует метод parse
, который выбрасывает ошибку в случае, если данные не прошли валидацию. Вот как выглядел бы код:
userSchema.parse({
name,
age,
email,
password
});
Теперь давайте напишем небольшой код, который будет валидировать поля по нажатию кнопки.
HTML:
<html>
<head>
<title>Validation</title>
<meta charset="UTF-8" />
</head>
<body>
<div id="app"></div>
<input type="text" id="name" placeholder="name" />
<input type="text" id="age" placeholder="age" />
<input type="text" id="email" placeholder="email" />
<input type="text" id="password" placeholder="password" />
<button id="validateButton">Validate</button>
<span id="validationErrors"></span>
<script src="src/index.ts"></script>
</body>
</html>
JavaScript
import { z } from "zod";
const passwordValidationRegex =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/;
const userSchema = z.object({
name: z.string(),
age: z.number().min(18),
email: z.string().email(),
password: z
.string()
.min(8, { message: "Пароль должен быть не менее 8 символов" })
.refine((val) => passwordValidationRegex.test(val), {
message:
"Пароль должен содержать минимум одну цифру, одну заглавную и одну строчную букву",
}),
});
document.getElementById("validateButton").addEventListener("click", () => {
const name = document.getElementById("name").value;
const age = parseInt(document.getElementById("age").value, 10);
const email = document.getElementById("email").value;
const password = document.getElementById("password").value;
try {
// Проверяем валидность данных
userSchema.parse({
name,
age,
email,
password,
});
document.getElementById("validationErrors").textContent =
"Все данные корректны";
} catch (error) {
// Если данные невалидны, то будет ошибка
document.getElementById("validationErrors").textContent = error.errors
.map((e) => e.message)
.join(", ");
}
});
Кода гораздо меньше, он куда более расширяемый и декларативный. Поддерживать его — сплошное удовольствие, чего не скажешь о подходе «в лоб», когда мы описываем всё через if-else
.
Если бы мы валидировали «в лоб», типы для схемы пришлось бы писать отдельно. Это не проблема, когда в ней всего 4 поля, но на практике полей бывает сотни. Поддерживать отдельно функцию для валидации и параллельно добавлять поля в интерфейс довольно утомительно. Zod позволяет просто взять и вывести тип из нашей схемы, а затем использовать его везде, где нужно.
Вывод типа с помощью утилиты z.infer и использование полученных данных:
import { z } from "zod";
const passwordValidationRegex =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/;
const userSchema = z.object({
name: z.string(),
age: z.number().min(18),
email: z.string().email(),
password: z
.string()
.min(8, { message: "Пароль должен быть не менее 8 символов" })
.refine((val) => passwordValidationRegex.test(val), {
message:
"Пароль должен содержать минимум одну цифру, одну заглавную и одну строчную букву",
}),
});
// вывели тип с помощью z.infer
// обратите внимание, что нам нужно взять тип объекта через typeof!
type UserType = z.infer<typeof userSchema>;
// Пример использования
function processUser(user: UserType) {
// Теперь вы можете быть уверены, что 'user' соответствует вашей схеме
console.log("Processing user:", user.name);
}
// Пример использования с валидными данными
try {
const validUser = userSchema.parse({
name: "Иван",
age: 30,
email: "ivan@example.com",
password: "Password123",
});
processUser(validUser);
} catch (error) {
console.error("Validation failed:", error);
}
В одном месте мы объявили и схему для валидации в рантайме, и тип, который пригодится нам на этапе разработки. Это же прекрасно: мы сделали двойную работу, написав меньше кода.
Обычно мы делаем программы, которые получают данные на вход из внешних источников. Во фронтенде это формы и сторонние API, формат данных которых может измениться со временем без нашего ведома. Например, сейчас бэкенд отдаёт всё в корректном формате, и мы на него рассчитываем, а позже формат поменяется. Если мы не будем валидировать ответ бэкенда, то можем сразу и не догадаться, в чём дело.
Хорошая валидация делает жизнь проще как для разработчиков, так и для пользователей. Использование инструментов, таких как Zod, делает этот процесс ещё более гладким и приятным.
Это как поставить на входе в приложение не просто стражника, а дружелюбного робота, который умеет шутить и всегда готов помочь. Поэтому, если вы хотите сделать ваше приложение удобным и безопасным, не забывайте о валидации данных :)