TypeStack/class-validator для TypeScript: полное руководство с примерами

Онлайн-тренажер Python для начинающих

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

Начать курс

TypeStack/class-validator для TypeScript: полное руководство с примерами

1. Что это и зачем нужна

Библиотека TypeStack/class-validator — это мощный инструмент для валидации данных в TypeScript и JavaScript, основанный на декораторах. Она позволяет разработчикам описывать правила валидации непосредственно в классах, используя декораторы свойств. Это делает код более читаемым, самодокументируемым и значительно упрощает обработку входящих данных, особенно при работе с REST API, формами или любыми другими источниками данных.

Зачем она нужна? В современной веб-разработке данные приходят из множества источников: HTTP-запросы, файлы конфигурации, базы данных. Без надёжной валидации эти данные могут привести к ошибкам времени выполнения, уязвимостям безопасности или просто к некорректной работе приложения. class-validator решает эту проблему, предоставляя декларативный способ проверки данных. Вместо того чтобы писать громоздкие if-else конструкции, вы просто добавляете декораторы к свойствам класса, и библиотека автоматически проверяет их соответствие заданным правилам.

Ещё одно ключевое преимущество — тесная интеграция с TypeScript. Декораторы позволяют не только валидировать данные, но и определять их типы, что делает процесс разработки более безопасным и предсказуемым. Библиотека поддерживает широкий спектр встроенных валидаторов (от простых проверок на длину строки до сложных регулярных выражений) и позволяет легко создавать собственные кастомные валидаторы.

2. Установка

Для установки библиотеки class-validator и её обязательной зависимости reflect-metadata выполните следующую команду в терминале вашего проекта:

npm install class-validator reflect-metadata

После установки необходимо убедиться, что в вашем файле tsconfig.json включены следующие опции для корректной работы декораторов:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

Также важно импортировать reflect-metadata в самом начале вашего приложения (обычно в главном файле index.ts или app.ts):

import "reflect-metadata";

3. Быстрый старт

Рассмотрим минимальный рабочий пример. Создадим класс пользователя с валидацией полей:

import "reflect-metadata";
import { validate, IsString, IsEmail, MinLength } from "class-validator";

class User {
    @IsString()
    @MinLength(3, { message: "Имя должно содержать минимум 3 символа" })
    name: string;

    @IsEmail({}, { message: "Некорректный email" })
    email: string;

    constructor(name: string, email: string) {
        this.name = name;
        this.email = email;
    }
}

async function main() {
    const user = new User("Jo", "invalid-email");
    const errors = await validate(user);

    if (errors.length > 0) {
        console.log("Ошибки валидации:");
        errors.forEach(error => {
            console.log(`- ${error.property}: ${Object.values(error.constraints).join(", ")}`);
        });
    } else {
        console.log("Валидация пройдена успешно!");
    }
}

main();
// Вывод:
// Ошибки валидации:
// - name: Имя должно содержать минимум 3 символа
// - email: Некорректный email

В этом примере мы создали класс User, добавили декораторы к его свойствам и вызвали функцию validate(). Она возвращает массив ошибок, если данные не соответствуют правилам.

4. Основные методы/функции/классы

Рассмотрим 8 ключевых функций и классов библиотеки class-validator.

4.1. validate(object: object, options?: ValidatorOptions): Promise<ValidationError[]>

Что делает: Основная функция для валидации объекта. Принимает экземпляр класса и возвращает массив ошибок ValidationError. Если ошибок нет, возвращается пустой массив.

import { validate, IsString } from "class-validator";

class Person {
    @IsString()
    name: string;

    constructor(name: string) {
        this.name = name;
    }
}

const person = new Person(123 as any);
validate(person).then(errors => {
    console.log(errors.length); // 1
});

4.2. validateOrReject(object: object, options?: ValidatorOptions): Promise<void>

Что делает: Аналог validate(), но в случае ошибок выбрасывает исключение (reject) с массивом ошибок. Удобно использовать с async/await и try/catch.

import { validateOrReject, IsNumber } from "class-validator";

class Product {
    @IsNumber()
    price: number;

    constructor(price: number) {
        this.price = price;
    }
}

async function checkProduct() {
    const product = new Product("not a number" as any);
    try {
        await validateOrReject(product);
        console.log("Валидация пройдена");
    } catch (errors) {
        console.log("Ошибка:", errors[0].constraints);
    }
}
checkProduct();

4.3. isDefined(value: any): boolean

Что делает: Проверяет, определено ли значение (не null и не undefined). Часто используется в кастомных валидаторах.

import { isDefined } from "class-validator";

console.log(isDefined("hello")); // true
console.log(isDefined(null));    // false
console.log(isDefined(undefined)); // false

4.4. isEmpty(value: any): boolean

Что делает: Проверяет, является ли значение "пустым". Для строк — пустая строка, для массивов — длина 0, для объектов — отсутствие ключей.

import { isEmpty } from "class-validator";

console.log(isEmpty(""));        // true
console.log(isEmpty([]));       // true
console.log(isEmpty({}));       // true
console.log(isEmpty("text"));   // false

4.5. isEmail(value: string): boolean

Что делает: Проверяет, является ли строка корректным email-адресом. Использует встроенное регулярное выражение.

import { isEmail } from "class-validator";

console.log(isEmail("user@example.com")); // true
console.log(isEmail("invalid-email"));    // false

4.6. isInt(value: any): boolean

Что делает: Проверяет, является ли значение целым числом. Не преобразует типы, то есть строка "123" вернёт false.

import { isInt } from "class-validator";

console.log(isInt(42));      // true
console.log(isInt(3.14));    // false
console.log(isInt("42"));    // false

4.7. isString(value: any): boolean

Что делает: Проверяет, является ли значение строкой. Аналог typeof value === 'string'.

import { isString } from "class-validator";

console.log(isString("hello")); // true
console.log(isString(123));     // false
console.log(isString(null));    // false

4.8. ValidationError (класс)

Что делает: Класс, представляющий одну ошибку валидации. Содержит свойства property (имя поля), constraints (объект с сообщениями об ошибках) и children (для вложенных объектов).

import { validate, IsString } from "class-validator";

class Test {
    @IsString()
    field: any;
}

const test = new Test();
test.field = 123;

validate(test).then(errors => {
    if (errors.length > 0) {
        const error = errors[0];
        console.log(error.property);    // "field"
        console.log(error.constraints); // { isString: "field must be a string" }
    }
});

5. Реальный практический пример

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

import "reflect-metadata";
import {
    validate,
    IsString,
    IsEmail,
    MinLength,
    MaxLength,
    IsOptional,
    IsArray,
    ValidateNested,
    IsEnum,
    Matches,
    ArrayMinSize,
    IsBoolean,
    Min,
    Max,
    IsInt
} from "class-validator";
import { Type } from "class-transformer";

// Перечисление для ролей пользователя
enum UserRole {
    ADMIN = "admin",
    USER = "user",
    MODERATOR = "moderator"
}

// Класс для адреса (вложенный объект)
class Address {
    @IsString({ message: "Страна должна быть строкой" })
    @MinLength(2, { message: "Страна должна содержать минимум 2 символа" })
    country: string;

    @IsString()
    @MinLength(2)
    city: string;

    @IsString()
    @Matches(/^\d{6}$/, { message: "Почтовый индекс должен состоять из 6 цифр" })
    zipCode: string;

    constructor(country: string, city: string, zipCode: string) {
        this.country = country;
        this.city = city;
        this.zipCode = zipCode;
    }
}

// Основной класс пользователя
class CreateUserDto {
    @IsString({ message: "Имя пользователя обязательно" })
    @MinLength(3, { message: "Имя пользователя должно быть минимум 3 символа" })
    @MaxLength(20, { message: "Имя пользователя должно быть максимум 20 символов" })
    username: string;

    @IsEmail({}, { message: "Пожалуйста, укажите корректный email" })
    email: string;

    @IsString()
    @MinLength(8, { message: "Пароль должен содержать минимум 8 символов" })
    @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {
        message: "Пароль должен содержать заглавные, строчные буквы и цифры"
    })
    password: string;

    @IsInt({ message: "Возраст должен быть целым числом" })
    @Min(18, { message: "Вам должно быть минимум 18 лет" })
    @Max(120, { message: "Некорректный возраст" })
    age: number;

    @IsOptional()
    @IsString()
    phone?: string;

    @IsEnum(UserRole, { message: "Роль должна быть admin, user или moderator" })
    role: UserRole;

    @IsBoolean({ message: "Согласие с условиями обязательно" })
    agreeToTerms: boolean;

    @ValidateNested()
    @Type(() => Address)
    address: Address;

    @IsOptional()
    @IsArray()
    @IsString({ each: true, message: "Каждый тег должен быть строкой" })
    @ArrayMinSize(1, { message: "Добавьте хотя бы один тег" })
    tags?: string[];

    constructor(
        username: string,
        email: string,
        password: string,
        age: number,
        role: UserRole,
        agreeToTerms: boolean,
        address: Address,
        tags?: string[]
    ) {
        this.username = username;
        this.email = email;
        this.password = password;
        this.age = age;
        this.role = role;
        this.agreeToTerms = agreeToTerms;
        this.address = address;
        this.tags = tags;
    }
}

// Функция для обработки регистрации
async function registerUser(userData: CreateUserDto) {
    const errors = await validate(userData, {
        validationError: { target: false, value: false },
        whitelist: true,
        forbidNonWhitelisted: true
    });

    if (errors.length > 0) {
        console.error("Ошибки валидации:");
        const formattedErrors = errors.map(error => ({
            field: error.property,
            messages: Object.values(error.constraints || {})
        }));
        console.error(JSON.stringify(formattedErrors, null, 2));
        return { success: false, errors: formattedErrors };
    }

    console.log("Пользователь успешно зарегистрирован:", userData.username);
    return { success: true, user: userData };
}

// Пример использования
async function main() {
    const address = new Address("Россия", "Москва", "123456");
    const user = new CreateUserDto(
        "john_doe",
        "john@example.com",
        "StrongPass1",
        25,
        UserRole.USER,
        true,
        address,
        ["javascript", "typescript"]
    );

    const result = await registerUser(user);
    console.log(result);

    // Пример с ошибками
    const badUser = new CreateUserDto(
        "jo",
        "invalid-email",
        "weak",
        15,
        "unknown" as UserRole,
        false,
        new Address("", "", "123"),
        []
    );

    const badResult = await registerUser(badUser);
    console.log(badResult);
}

main();

Этот пример демонстрирует: вложенную валидацию (@ValidateNested), кастомные сообщения об ошибках, проверку перечислений (@IsEnum), регулярные выражения (@Matches), опциональные поля (@IsOptional) и массивы с валидацией элементов (each: true).

6. Советы по производительности и типичные ошибки

Советы по производительности

Рекомендаций