Дослідіть світ валідації TypeScript під час виконання. Відкрийте для себе провідні бібліотеки, найкращі практики та практичні приклади для створення надійніших застосунків.
Валідація TypeScript: Освоєння бібліотек перевірки типів під час виконання для надійних застосунків
Оскільки застосунки зростають у складності та розгортаються в різних глобальних середовищах, забезпечення цілісності даних та запобігання несподіваним помилкам стає першочерговим завданням. Хоча TypeScript чудово справляється з перевіркою типів під час компіляції, виявляючи помилки ще до запуску коду, існують сценарії, коли валідація під час виконання є незамінною. Це особливо актуально при роботі із зовнішніми джерелами даних, такими як запити API, введення користувача або файли конфігурації, де форма та типи даних не гарантовані.
Цей вичерпний посібник занурюється у критичну область валідації TypeScript під час виконання. Ми розглянемо, чому це необхідно, представимо провідні бібліотеки, які дозволяють розробникам впроваджувати надійні стратегії валідації, та надамо практичні приклади, щоб допомогти вам створювати більш стійкі застосунки для вашої міжнародної аудиторії.
Чому перевірка типів під час виконання є критично важливою в TypeScript
Статична типізація TypeScript є потужним інструментом. Вона дозволяє нам визначати очікувані структури даних та типи, і компілятор буде відзначати розбіжності під час розробки. Однак інформація про типи TypeScript переважно стирається під час процесу компіляції в JavaScript. Це означає, що як тільки ваш код запускається, рушій JavaScript не має внутрішнього знання про визначені вами типи TypeScript.
Розгляньте ці сценарії, де валідація під час виконання стає необхідною:
- Відповіді API: Дані, отримані від зовнішніх API, навіть тих, що мають задокументовані схеми, іноді можуть відхилятися від очікувань через непередбачені проблеми, зміни в реалізації постачальника API або мережеві помилки.
- Введення користувача: Форми та інтерфейси користувача збирають дані, які потрібно перевірити перед обробкою, гарантуючи, що приймаються лише дійсні та очікувані формати. Це критично важливо для міжнародних застосунків, де формати введення (наприклад, телефонні номери або дати) можуть значно відрізнятися.
- Файли конфігурації: Застосунки часто покладаються на файли конфігурації (наприклад, JSON, YAML). Валідація цих файлів під час запуску гарантує, що застосунок налаштований правильно, запобігаючи збоям або неправильній поведінці.
- Дані з ненадійних джерел: При взаємодії з даними, що походять з потенційно ненадійних джерел, ретельна валідація є заходом безпеки для запобігання атак ін'єкцій або пошкодження даних.
- Крос-середовищна узгодженість: Забезпечення узгодженості структур даних у різних середовищах виконання JavaScript (Node.js, браузери) та під час серіалізації/десеріалізації (наприклад, JSON.parse/stringify) є життєво важливим.
Без валідації під час виконання ваш застосунок може зіткнутися з несподіваними даними, що призведе до помилок під час виконання, пошкодження даних, вразливостей безпеки та поганого досвіду користувача. Це особливо проблематично в глобальному контексті, де дані можуть надходити з різних систем і відповідати різним регіональним стандартам.
Ключові бібліотеки для валідації TypeScript під час виконання
На щастя, екосистема TypeScript пропонує кілька чудових бібліотек, спеціально розроблених для перевірки типів під час виконання та валідації даних. Ці бібліотеки дозволяють визначати схеми, які описують ваші очікувані структури даних, а потім використовувати ці схеми для валідації вхідних даних.
Ми розглянемо деякі з найпопулярніших та найефективніших бібліотек:
1. Zod
Zod швидко здобув популярність завдяки своєму інтуїтивно зрозумілому API, сильній інтеграції з TypeScript та вичерпному набору функцій. Він дозволяє визначати “схему” для ваших даних, а потім використовувати цю схему для розбору та валідації даних під час виконання. Схеми Zod є сильно типізованими, що означає, що типи TypeScript можуть бути виведені безпосередньо з визначення схеми, мінімізуючи потребу в ручних анотаціях типів.
Ключові особливості Zod:
- Інференційна типізація: Виведення типів TypeScript безпосередньо зі схем Zod.
- Декларативне визначення схеми: Визначення складних структур даних, включаючи вкладені об'єкти, масиви, об'єднання, перетини та користувацькі типи, у чіткий і зрозумілий спосіб.
- Потужна трансформація: Перетворення даних під час розбору (наприклад, рядок у число, розбір дати).
- Комплексна звітність про помилки: Надає детальні та зручні для користувача повідомлення про помилки, що є критично важливим для налагодження та надання зворотного зв'язку користувачам у всьому світі.
- Вбудовані валідатори: Пропонує широкий спектр вбудованих валідаторів для рядків, чисел, булевих значень, дат та багато іншого, а також можливість створювати користувацькі валідатори.
- API, що об'єднується в ланцюжок: Схеми легко компонуються та розширюються.
Приклад: Валідація профілю користувача за допомогою Zod
Уявімо, що ми отримуємо дані профілю користувача з API. Ми хочемо переконатися, що користувач має дійсне ім'я, необов'язковий вік та список інтересів.
import { z } from 'zod';
// Define the schema for a User Profile
const UserProfileSchema = z.object({
name: z.string().min(1, "Name cannot be empty."), // Name is a required string, at least 1 character
age: z.number().int().positive().optional(), // Age is an optional positive integer
interests: z.array(z.string()).min(1, "At least one interest is required."), // Interests is an array of strings, at least one item
isActive: z.boolean().default(true) // isActive is a boolean, defaults to true if not provided
});
// Infer the TypeScript type from the schema
type UserProfile = z.infer<typeof UserProfileSchema>;
// Example API response data
const apiResponse1 = {
name: "Alice",
age: 30,
interests: ["coding", "travel"],
isActive: false
};
const apiResponse2 = {
name: "Bob",
// age is missing
interests: [] // empty interests array
};
// --- Validation Example 1 ---
try {
const validatedProfile1 = UserProfileSchema.parse(apiResponse1);
console.log('Profile 1 is valid:', validatedProfile1);
// TypeScript now knows validatedProfile1 has the type UserProfile
} catch (error) {
if (error instanceof z.ZodError) {
console.error('Validation errors for Profile 1:', error.errors);
} else {
console.error('An unexpected error occurred:', error);
}
}
// --- Validation Example 2 ---
try {
const validatedProfile2 = UserProfileSchema.parse(apiResponse2);
console.log('Profile 2 is valid:', validatedProfile2);
} catch (error) {
if (error instanceof z.ZodError) {
console.error('Validation errors for Profile 2:', error.errors);
/*
Expected output for errors:
[
{ code: 'array_min_size', message: 'At least one interest is required.', path: [ 'interests' ] }
]
*/
} else {
console.error('An unexpected error occurred:', error);
}
}
// --- Example with optional property behavior ---
const apiResponse3 = {
name: "Charlie",
interests: ["reading"]
// isActive is omitted, will default to true
};
try {
const validatedProfile3 = UserProfileSchema.parse(apiResponse3);
console.log('Profile 3 is valid (isActive defaults to true):', validatedProfile3);
/*
Expected output: {
name: 'Charlie',
interests: [ 'reading' ],
isActive: true
}
*/
}
catch (error) {
console.error('Validation errors for Profile 3:', error);
}
Звітність Zod про помилки особливо корисна для міжнародних застосунків, оскільки ви можете інтернаціоналізувати самі повідомлення про помилки на основі локалі користувача, хоча сама бібліотека надає структуровані дані про помилки, що робить цей процес простим.
2. Yup
Yup – ще одна дуже популярна та зріла бібліотека валідації для JavaScript та TypeScript. Вона часто використовується з Formik для валідації форм, але так само потужна для загальної валідації даних. Yup використовує плавний API для визначення схем, які потім використовуються для валідації об'єктів JavaScript.
Ключові особливості Yup:
- Валідація на основі схеми: Визначення схем даних за допомогою ланцюжкового, декларативного синтаксису.
- Виведення типів: Може виводити типи TypeScript, хоча в деяких випадках може вимагати більш явних визначень типів порівняно з Zod.
- Багатий набір валідаторів: Підтримує валідацію для різних типів даних, включаючи рядки, числа, дати, масиви, об'єкти та багато іншого.
- Умовна валідація: Дозволяє встановлювати правила валідації, що залежать від значень інших полів.
- Настроювані повідомлення про помилки: Легко визначайте користувацькі повідомлення про помилки для невдалих валідацій.
- Кросплатформна сумісність: Безперебійно працює в середовищах Node.js та браузера.
Приклад: Валідація запису каталогу товарів за допомогою Yup
Давайте перевіримо запис товару, переконавшись, що він має назву, ціну та необов'язковий опис.
import * as yup from 'yup';
// Define the schema for a Product Entry
const ProductSchema = yup.object({
name: yup.string().required('Product name is required.'),
price: yup.number().positive('Price must be a positive number.').required('Price is required.'),
description: yup.string().optional('Description is optional.'),
tags: yup.array(yup.string()).default([]), // Default to an empty array if not provided
releaseDate: yup.date().optional()
});
// Infer the TypeScript type from the schema
type Product = yup.InferType<typeof ProductSchema>;
// Example product data
const productData1 = {
name: "Global Gadget",
price: 199.99,
tags: ["electronics", "new arrival"],
releaseDate: new Date('2023-10-27T10:00:00Z')
};
const productData2 = {
name: "Budget Widget",
price: -10.50 // Invalid price
};
// --- Validation Example 1 ---
ProductSchema.validate(productData1, { abortEarly: false })
.then(function (validProduct: Product) {
console.log('Product 1 is valid:', validProduct);
// TypeScript knows validProduct is of type Product
})
.catch(function (err: yup.ValidationError) {
console.error('Validation errors for Product 1:', err.errors);
});
// --- Validation Example 2 ---
ProductSchema.validate(productData2, { abortEarly: false })
.then(function (validProduct: Product) {
console.log('Product 2 is valid:', validProduct);
})
.catch(function (err: yup.ValidationError) {
console.error('Validation errors for Product 2:', err.errors);
/*
Expected output for errors:
[
'Price must be a positive number.'
]
*/
});
// --- Example with default value behavior ---
const productData3 = {
name: "Simple Item",
price: 5.00
// tags and releaseDate are omitted
};
ProductSchema.validate(productData3, { abortEarly: false })
.then(function (validProduct: Product) {
console.log('Product 3 is valid (tags default to []):', validProduct);
/*
Expected output: {
name: 'Simple Item',
price: 5,
tags: [],
releaseDate: undefined
}
*/
})
.catch(function (err: yup.ValidationError) {
console.error('Validation errors for Product 3:', err.errors);
});
Розширена документація Yup та велика спільнота роблять її надійним вибором, особливо для проєктів з існуючим використанням Yup або тих, хто потребує тонкого контролю над звітністю про помилки та складними потоками валідації.
3. io-ts
io-ts – це бібліотека, яка надає валідацію типів під час виконання для TypeScript, використовуючи функціональний підхід до програмування. Вона визначає “кодеки”, які використовуються для кодування та декодування даних, забезпечуючи відповідність даних певному типу під час виконання. Ця бібліотека відома своєю суворістю та сильним дотриманням функціональних принципів.
Ключові особливості io-ts:
- На основі кодеків: Використовує кодеки для визначення та валідації типів.
- Парадигма функціонального програмування: Добре узгоджується зі стилями функціонального програмування.
- Типова безпека під час виконання: Забезпечує гарантовану типову безпеку під час виконання.
- Розширюваність: Дозволяє створювати користувацькі кодеки.
- Розширений набір функцій: Підтримує об'єднані типи, перетинані типи, рекурсивні типи та багато іншого.
- Супутні бібліотеки: Має супутні бібліотеки, такі як
io-ts-promiseдля легшої інтеграції промісів таio-ts-reportersдля кращої звітності про помилки.
Приклад: Валідація точки геолокації за допомогою io-ts
Валідація географічних координат є поширеним завданням, особливо для глобальних застосунків, що враховують місцезнаходження.
import * as t from 'io-ts';
import { formatValidationErrors } from 'io-ts-reporters'; // For better error reporting
// Define the codec for a Geolocation Point
const GeolocationPoint = t.type({
latitude: t.number,
longitude: t.number,
accuracy: t.union([t.number, t.undefined]) // accuracy is optional
});
// Infer the TypeScript type from the codec
type Geolocation = t.TypeOf<typeof GeolocationPoint>;
// Example geolocation data
const geoData1 = {
latitude: 34.0522,
longitude: -118.2437,
accuracy: 10.5
};
const geoData2 = {
latitude: 'not a number',
longitude: -0.1278
};
// --- Validation Example 1 ---
const result1 = GeolocationPoint.decode(geoData1);
if (result1._tag === 'Right') {
const validatedGeo1: Geolocation = result1.right;
console.log('Geolocation 1 is valid:', validatedGeo1);
} else {
// result1._tag === 'Left'
console.error('Validation errors for Geolocation 1:', formatValidationErrors(result1.left));
}
// --- Validation Example 2 ---
const result2 = GeolocationPoint.decode(geoData2);
if (result2._tag === 'Right') {
const validatedGeo2: Geolocation = result2.right;
console.log('Geolocation 2 is valid:', validatedGeo2);
} else {
// result2._tag === 'Left'
console.error('Validation errors for Geolocation 2:', formatValidationErrors(result2.left));
/*
Expected output for errors (using io-ts-reporters):
- latitude: Expected number but received String
*/
}
// --- Example with optional property behavior ---
const geoData3 = {
latitude: 51.5074, // London
longitude: -0.1278
// accuracy is omitted
};
const result3 = GeolocationPoint.decode(geoData3);
if (result3._tag === 'Right') {
const validatedGeo3: Geolocation = result3.right;
console.log('Geolocation 3 is valid (accuracy is undefined):', validatedGeo3);
/*
Expected output: {
latitude: 51.5074,
longitude: -0.1278,
accuracy: undefined
}
*/
}
else {
console.error('Validation errors for Geolocation 3:', formatValidationErrors(result3.left));
}
io-ts є потужним вибором для проєктів, які використовують принципи функціонального програмування та потребують високого ступеня впевненості у типовій безпеці під час виконання. Його детальна звітність про помилки, особливо в поєднанні з io-ts-reporters, є безцінною для налагодження інтернаціоналізованих застосунків.
4. class-validator
class-validator та його супутник class-transformer чудово підходять для сценаріїв, де ви працюєте з класами, особливо у фреймворках, таких як NestJS. Він дозволяє визначати правила валідації за допомогою декораторів безпосередньо у властивостях класу.
Ключові особливості class-validator:
- Валідація на основі декораторів: Використання декораторів (наприклад,
@IsEmail(),@IsNotEmpty()) у властивостях класу. - Інтеграція з Class-Transformer: Бездоганне перетворення вхідних даних у екземпляри класу перед валідацією.
- Розширюваність: Створення користувацьких декораторів валідації.
- Вбудовані валідатори: Широкий спектр декораторів для поширених потреб валідації.
- Обробка помилок: Надає детальні об'єкти помилок валідації.
Приклад: Валідація форми реєстрації електронної пошти за допомогою class-validator
Це особливо корисно для серверних API, що обробляють реєстрацію користувачів з усього світу.
import 'reflect-metadata'; // Required for decorators
import { validate, Contains, IsInt, Length, IsEmail, IsOptional } from 'class-validator';
import { plainToClass, classToPlain } from 'class-transformer';
// Define the DTO (Data Transfer Object) with validation decorators
class UserRegistrationDto {
@Length(5, 50, { message: 'Username must be between 5 and 50 characters.' })
username: string;
@IsEmail({}, { message: 'Invalid email address format.' })
email: string;
@IsInt({ message: 'Age must be an integer.' })
@IsOptional() // Age is optional
age?: number;
constructor(username: string, email: string, age?: number) {
this.username = username;
this.email = email;
this.age = age;
}
}
// Example incoming data (e.g., from an API request body)
const registrationData1 = {
username: "global_user",
email: "user@example.com",
age: 25
};
const registrationData2 = {
username: "short", // Too short username
email: "invalid-email", // Invalid email
age: 30.5 // Not an integer
};
// --- Validation Example 1 ---
// First, transform plain object into a class instance
const userDto1 = plainToClass(UserRegistrationDto, registrationData1);
validate(userDto1).then(errors => {
if (errors.length > 0) {
console.error('Validation errors for Registration 1:', errors);
} else {
console.log('Registration 1 is valid:', classToPlain(userDto1)); // Convert back to plain object for output
}
});
// --- Validation Example 2 ---
const userDto2 = plainToClass(UserRegistrationDto, registrationData2);
validate(userDto2).then(errors => {
if (errors.length > 0) {
console.error('Validation errors for Registration 2:', errors.map(err => err.constraints));
/*
Expected output for errors.constraints:
[ {
length: 'Username must be between 5 and 50 characters.',
isEmail: 'Invalid email address format.',
isInt: 'Age must be an integer.'
} ]
*/
} else {
console.log('Registration 2 is valid:', classToPlain(userDto2));
}
});
// --- Example with optional property behavior ---
const registrationData3 = {
username: "validUser",
email: "valid@example.com"
// age is omitted, which is allowed by @IsOptional()
};
const userDto3 = plainToClass(UserRegistrationDto, registrationData3);
validate(userDto3).then(errors => {
if (errors.length > 0) {
console.error('Validation errors for Registration 3:', errors);
} else {
console.log('Registration 3 is valid (age is undefined):', classToPlain(userDto3));
/*
Expected output: {
username: 'validUser',
email: 'valid@example.com',
age: undefined
}
*/
}
});
class-validator особливо ефективний у серверних застосунках або фреймворках, які сильно покладаються на класи та об'єктно-орієнтоване програмування. Його синтаксис на основі декораторів є дуже виразним та зручним для розробників.
Вибір правильної бібліотеки валідації
Найкраща бібліотека валідації для вашого проєкту залежить від кількох факторів:
- Парадигма проєкту: Якщо ви активно займаєтеся функціональним програмуванням,
io-tsможе стати вашим вибором. Для об'єктно-орієнтованих підходівclass-validatorяскраво проявляє себе. Для більш загального, декларативного підходу з чудовим виведенням типів TypeScript,Zodє сильним претендентом.Yupпропонує зрілий та гнучкий API, придатний для багатьох сценаріїв. - Інтеграція з TypeScript:
Zodлідирує у безшовній інференції типів TypeScript безпосередньо зі схем. Інші пропонують хорошу інтеграцію, але можуть вимагати більш явних визначень типів. - Крива навчання:
ZodтаYup, як правило, вважаються легшими для новачків.io-tsмає крутішу криву навчання через свою функціональну природу.class-validatorє простим, якщо ви знайомі з декораторами. - Екосистема та спільнота:
YupтаZodмають великі та активні спільноти, що надають достатньо ресурсів та підтримки. - Специфічні функції: Якщо вам потрібні певні функції, такі як складні трансформації (
Zod), інтеграція форм (Yup) або валідація на основі декораторів (class-validator), це може вплинути на ваше рішення.
Для багатьох сучасних проєктів TypeScript Zod часто досягає золотої середини завдяки чудовому виведенню типів, інтуїтивно зрозумілому API та потужним функціям. Однак, не ігноруйте сильні сторони інших бібліотек.
Найкращі практики для валідації під час виконання
Ефективна реалізація валідації під час виконання вимагає більше, ніж просто вибір бібліотеки. Ось деякі найкращі практики, яких слід дотримуватися:
1. Валідуйте рано, валідуйте часто
Чим раніше ви перевіряєте дані, тим швидше ви можете виявити помилки. Цей принцип часто узагальнюється як “швидкий провал”. Валідуйте дані, як тільки вони надходять у вашу систему, незалежно від того, чи це запит API, введення користувача або файл конфігурації.
2. Централізуйте логіку валідації
Уникайте розкидання логіки валідації по всьому коду. Визначайте свої схеми або правила валідації у спеціалізованих модулях або класах. Це робить ваш код більш організованим, легшим в обслуговуванні та зменшує дублювання.
3. Використовуйте описові повідомлення про помилки
Повідомлення про помилки валідації повинні бути інформативними. Для міжнародних застосунків це означає, що повідомлення про помилки повинні бути:
- Чіткими та лаконічними: Легко зрозумілими для користувачів незалежно від їхнього технічного рівня.
- Дійсними: Направляти користувача, як виправити введення.
- Локалізованими: Створіть свою систему таким чином, щоб дозволити переклад повідомлень про помилки на основі локалі користувача. Структуровані помилки, надані бібліотеками валідації, є ключем до забезпечення цього.
Наприклад, замість просто “Недійсний ввід” використовуйте “Будь ласка, введіть дійсну адресу електронної пошти у форматі example@domain.com”. Для міжнародних користувачів це може бути локалізовано їхньою мовою та регіональними правилами електронної пошти.
4. Визначайте схеми, які відповідають вашим типам TypeScript
Прагніть до узгодженості між вашими типами TypeScript та вашими схемами валідації під час виконання. Бібліотеки, такі як Zod, чудово виводять типи зі схем, що є ідеальним сценарієм. Якщо ви вручну визначаєте типи та схеми окремо, переконайтеся, що вони синхронізовані, щоб уникнути розбіжностей.
5. Обробляйте помилки валідації коректно
Не дозволяйте помилкам валідації призводити до збоїв вашого застосунку. Впроваджуйте надійну обробку помилок. Для кінцевих точок API повертайте відповідні коди стану HTTP (наприклад, 400 Bad Request) та структуровану відповідь JSON з деталями помилок. Для інтерфейсів користувача відображайте чіткі повідомлення про помилки поруч із відповідними полями форми.
6. Розгляньте валідацію на різних рівнях
Валідація на стороні клієнта надає миттєвий зворотний зв'язок користувачам, покращуючи користувацький досвід. Однак вона не є безпечною, оскільки її можна обійти. Валідація на стороні сервера є важливою для цілісності даних та безпеки, оскільки вона є останньою лінією захисту. Завжди впроваджуйте валідацію на стороні сервера, навіть якщо у вас є валідація на стороні клієнта.
7. Використовуйте виведення типів TypeScript
Використовуйте бібліотеки, які забезпечують сильну інтеграцію з TypeScript. Це зменшує шаблонний код та гарантує, що ваші схеми валідації та типи TypeScript завжди синхронізовані. Коли бібліотека може виводити типи зі схем (як Zod), це є значною перевагою.
8. Глобальні міркування: часові пояси, валюти та формати
При створенні для глобальної аудиторії правила валідації повинні враховувати регіональні відмінності:
- Дати та час: Перевіряйте дати та час відповідно до очікуваних форматів (наприклад, ДД/ММ/РРРР проти ММ/ДД/РРРР) та правильно обробляйте перетворення часових поясів. Бібліотеки, такі як Zod, мають вбудовані парсери дат, які можна налаштувати.
- Валюти: Перевіряйте значення валют, потенційно включаючи специфічні вимоги до точності або коди валют.
- Телефонні номери: Впроваджуйте надійну валідацію для міжнародних телефонних номерів, враховуючи коди країн та різні формати. Бібліотеки, такі як
libphonenumber-js, можуть використовуватися в поєднанні зі схемами валідації. - Адреси: Валідація компонентів адреси може бути складною через значні міжнародні відмінності у структурі та необхідних полях.
Ваші схеми валідації повинні бути достатньо гнучкими, щоб обробляти ці варіації, або достатньо специфічними для цільових ринків, які ви обслуговуєте.
Висновок
Хоча компіляційна перевірка TypeScript є наріжним каменем сучасної веб-розробки, перевірка типів під час виконання є не менш життєво важливим компонентом для створення надійних, безпечних та підтримуваних застосунків, особливо в глобальному контексті. Використовуючи потужні бібліотеки, такі як Zod, Yup, io-ts та class-validator, ви можете забезпечити цілісність даних, запобігти несподіваним помилкам та надати більш надійний досвід користувачам у всьому світі.
Використання цих стратегій валідації та найкращих практик призведе до більш стійких застосунків, які можуть витримати складнощі різноманітних джерел даних та взаємодій з користувачами в різних регіонах та культурах. Інвестуйте в ретельну валідацію; це інвестиція в якість та надійність вашого програмного забезпечення.