Сложные условные конструкции

Сложные условные конструкции в Class-validator формируют основу для моделей, где корректность одного поля зависит от состояния других полей, внешнего контекста или бизнес-правил. В таких сценариях простых декораторов типа @IsString() или @IsInt() недостаточно — требуется управление логикой проверки через условия, группы и пользовательские валидаторы.

Основной механизм условной валидации строится вокруг @ValidateIf, а также пользовательских декораторов через ValidatorConstraint.


@ValidateIf как базовый инструмент условной логики

@ValidateIf позволяет полностью включить или отключить валидацию поля на основе функции-условия.

import { ValidateIf, IsString, IsNotEmpty } from 'class-validator';

class UserDto {
  @ValidateIf(o => o.isCompany === true)
  @IsString()
  companyName;

  @ValidateIf(o => o.isCompany === false)
  @IsString()
  firstName;

  @ValidateIf(o => o.isCompany === false)
  @IsString()
  lastName;

  isCompany;
}

Логика работы строится вокруг объекта валидации o, который представляет текущий экземпляр класса.

Ключевая особенность: если ValidateIf возвращает false, все последующие декораторы поля полностью игнорируются.

Это делает возможной реализацию взаимоисключающих структур данных без необходимости писать сложную внешнюю логику.


Взаимосвязанные поля и зависимые ограничения

Одной из наиболее распространённых задач является проверка зависимости между полями.

Пример: пароль и подтверждение пароля

import { ValidateIf, Equals, IsString, MinLength } from 'class-validator';

class RegisterDto {
  @IsString()
  @MinLength(8)
  password;

  @ValidateIf(o => o.password !== undefined)
  @Equals('password')
  confirmPassword;
}

Однако такой подход ограничен, так как @Equals не предназначен для сравнения с другим полем. Более корректный вариант — пользовательский валидатор.


Пользовательские валидаторы для межполевой логики

Class-validator предоставляет механизм создания кастомных правил через ValidatorConstraint.

Базовая структура

import {
  ValidatorConstraint,
  ValidatorConstraintInterface,
  ValidationArguments,
  registerDecorator,
} from 'class-validator';

Реализация сравнения двух полей

@ValidatorConstraint({ name: 'Match', async: false })
class MatchConstraint {
  validate(value, args) {
    const [relatedPropertyName] = args.constraints;
    const relatedValue = args.object[relatedPropertyName];
    return value === relatedValue;
  }

  defaultMessage(args) {
    return `${args.property} must match ${args.constraints[0]}`;
  }
}

Декоратор-обёртка

function Match(property) {
  return function (object, propertyName) {
    registerDecorator({
      target: object.constructor,
      propertyName,
      constraints: [property],
      validator: MatchConstraint,
    });
  };
}

Использование

class RegisterDto {
  password;

  @Match('password')
  confirmPassword;
}

Условная обязательность полей

Вместо жёсткой схемы «обязательно/необязательно» используется динамическая логика.

Пример: обязательность в зависимости от другого поля

import { ValidateIf, IsNotEmpty, IsEmail } from 'class-validator';

class ContactDto {
  @ValidateIf(o => o.preferredContact === 'email')
  @IsEmail()
  email;

  @ValidateIf(o => o.preferredContact === 'phone')
  @IsNotEmpty()
  phone;

  preferredContact;
}

Такая структура часто используется в формах с переключаемыми режимами ввода.


Инверсия логики через @IsOptional

@IsOptional не является полноценным условным оператором, но часто участвует в комбинированных конструкциях.

import { IsOptional, IsString, Length } from 'class-validator';

class UpdateUserDto {
  @IsOptional()
  @IsString()
  @Length(3, 20)
  username;
}

Отличие от @ValidateIf

  • @IsOptional игнорирует undefined и null
  • @ValidateIf управляет всей цепочкой валидаторов
  • @ValidateIf работает на уровне условия
  • @IsOptional работает на уровне значения

Комплексные условия с несколькими полями

Когда логика зависит от нескольких параметров, условия становятся составными.

Пример: доступ по роли и статусу

class AccessDto {
  @ValidateIf(o => o.role === 'admin' && o.isActive === true)
  @IsString()
  adminToken;

  role;

  isActive;
}

Использование промежуточной функции

При усложнении логики вынос условия повышает читаемость:

function canAccessAdminFields(obj) {
  return obj.role === 'admin' && obj.isActive === true;
}

class AccessDto {
  @ValidateIf(canAccessAdminFields)
  @IsString()
  adminToken;

  role;
  isActive;
}

Вложенные объекты и условная валидация

Class-validator поддерживает рекурсивную валидацию через @ValidateNested, которая также может быть условной.

import { ValidateNested, ValidateIf } from 'class-validator';
import { Type } from 'class-transformer';

class Address {
  street;
  city;
}

class UserDto {
  @ValidateIf(o => o.hasAddress === true)
  @ValidateNested()
  @Type(() => Address)
  address;

  hasAddress;
}

В таких структурах важно учитывать порядок преобразования через class-transformer, так как условия могут зависеть от уже преобразованных объектов.


Асинхронные условные проверки

Условие может включать асинхронную логику, если используется async: true в кастомном валидаторе.

Пример: проверка существования пользователя в базе

@ValidatorConstraint({ name: 'UniqueEmail', async: true })
class UniqueEmailConstraint {
  async validate(email, args) {
    const user = await fakeDatabaseFind(email);
    return !user;
  }
}

Комбинация с условием

class UserDto {
  @ValidateIf(o => o.allowRegistration === true)
  @UniqueEmail()
  email;

  allowRegistration;
}

Сложные деревья условий

В реальных схемах часто встречаются многоуровневые зависимости:

  • поле A включает проверку поля B
  • поле B зависит от поля C
  • поле C зависит от внешнего контекста

Пример каскадной логики

class OrderDto {
  @ValidateIf(o => o.type === 'delivery')
  @IsNotEmpty()
  address;

  @ValidateIf(o => o.type === 'delivery' && o.priority === 'high')
  @IsNotEmpty()
  courierCode;

  type;
  priority;
}

Каждый уровень добавляет уточнение к предыдущему условию, формируя цепочку зависимостей.


Пользовательские валидаторы с доступом к объекту целиком

ValidationArguments предоставляет доступ ко всему объекту, что позволяет строить сложные правила.

class RangeConstraint {
  validate(value, args) {
    const obj = args.object;
    return value >= obj.min && value <= obj.max;
  }
}

Применение

class RangeDto {
  min;
  max;

  @IsNumber()
  rangeValue;
}

Такой подход часто используется для бизнес-правил, где поля образуют единое ограничение.


Группы валидации как альтернатива условным конструкциям

Validation groups позволяют разделять правила без if.

import { IsString } from 'class-validator';

class UserDto {
  @IsString({ groups: ['create'] })
  password;

  @IsString({ groups: ['update'] })
  id;
}

Комбинация групп и условий

Группы могут использоваться совместно с @ValidateIf, формируя гибридную систему:

@ValidateIf(o => o.mode === 'strict')
@IsString({ groups: ['strict'] })
value;

Типичные ошибки при построении условной логики

1. Перегрузка @ValidateIf

Чрезмерно сложные условия внутри декоратора снижают предсказуемость:

@ValidateIf(o => o.a && o.b && (o.c || o.d) && o.e !== 'x')

Такие выражения затрудняют тестирование и сопровождение.


2. Игнорирование порядка трансформации

При использовании class-transformer условие может срабатывать до преобразования типов, что приводит к неожиданным результатам.


3. Смешивание @IsOptional и @ValidateIf

Комбинации могут давать неочевидное поведение:

  • @IsOptional пропускает undefined
  • @ValidateIf отключает всю цепочку

Использование обоих без необходимости приводит к дублированию логики.


Составные модели с бизнес-логикой

При построении сложных DTO часто формируется архитектура, где:

  • базовые поля валидируются стандартными декораторами
  • условные зависимости обрабатываются через @ValidateIf
  • межполевые связи реализуются кастомными валидаторами
  • контекстные сценарии разделяются через группы

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