Передача параметров в кастомный декоратор

Кастомные декораторы в Class-validator строятся на основе функций-фабрик, которые позволяют инкапсулировать логику валидации и одновременно передавать в неё параметры извне. Такой подход делает декораторы переиспользуемыми и конфигурируемыми, превращая их в полноценные инструменты описания бизнес-правил на уровне модели.


Фабрика декоратора как точка входа параметров

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

import { registerDecorator, ValidationOptions, ValidationArguments } fr om "class-validator";

export function IsPositiveWithThreshold(threshold: number, options?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      name: "IsPositiveWithThreshold",
      target: object.constructor,
      propertyName: propertyName,
      options: options,
      constraints: [threshold],
      validator: {
        validate(value: any, args: ValidationArguments) {
          const [min] = args.constraints;
          return typeof value === "number" && value > 0 && value >= min;
        }
      }
    });
  };
}

Ключевой момент заключается в том, что параметр threshold не передаётся напрямую в validate, а сохраняется в constraints.


Механизм constraints как канал передачи параметров

Поле constraints в registerDecorator является стандартным механизмом передачи произвольных значений в валидатор.

constraints: [threshold]

Эти значения затем доступны через объект ValidationArguments:

validate(value: any, args: ValidationArguments) {
  const [min] = args.constraints;
  return value >= min;
}

Структура constraints позволяет передавать несколько параметров одновременно:

constraints: [min, max, step]

и извлекать их по позиции:

const [min, max, step] = args.constraints;

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


Использование замыканий для передачи параметров

Помимо constraints, параметры декоратора доступны через замыкание. Это позволяет комбинировать два подхода: фиксированные параметры и динамические данные из объекта.

export function StartsWith(prefix: string, options?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      name: "StartsWith",
      target: object.constructor,
      propertyName,
      options,
      validator: {
        validate(value: any) {
          return typeof value === "string" && value.startsWith(prefix);
        }
      }
    });
  };
}

Здесь prefix доступен напрямую внутри validate, без использования constraints. Такой подход удобен, когда параметров немного и они не требуют структурирования.


ValidationArguments как источник контекста

ValidationArguments расширяет возможности параметризации, предоставляя доступ к:

  • объекту, в котором находится свойство (object)
  • имени проверяемого свойства (property)
  • значению свойства (value)
  • массиву constraints
validate(value: any, args: ValidationArguments) {
  const object = args.object;
  const propertyName = args.property;
  const [limit] = args.constraints;
}

Это позволяет строить декораторы, зависящие не только от переданных параметров, но и от состояния всего объекта.


Передача сложных параметров через объект

Когда параметров становится больше двух-трёх, использование массива constraints теряет читаемость. В таких случаях применяется упаковка параметров в объект.

type RangeOptions = {
  min: number;
  max: number;
  inclusive?: boolean;
};

Фабрика декоратора:

export function IsInRange(range: RangeOptions, options?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      name: "IsInRange",
      target: object.constructor,
      propertyName,
      options,
      constraints: [range],
      validator: {
        validate(value: any, args: ValidationArguments) {
          const config = args.constraints[0] as RangeOptions;

          if (config.inclusive) {
            return value >= config.min && value <= config.max;
          }

          return value > config.min && value < config.max;
        }
      }
    });
  };
}

Такой способ обеспечивает расширяемость без изменения сигнатуры декоратора.


Комбинирование параметров и ValidationOptions

Второй аргумент фабрики декоратора обычно используется для передачи стандартных опций валидации:

  • сообщения об ошибках
  • группы валидации
  • условные правила (each, always и др.)
export function IsEven(divisor: number, options?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      name: "IsEven",
      target: object.constructor,
      propertyName,
      options,
      constraints: [divisor],
      validator: {
        validate(value: number) {
          return value % divisor === 0;
        }
      }
    });
  };
}

Здесь options не участвуют в логике проверки, но влияют на поведение системы валидации в целом.


Типизация параметров в TypeScript

Типизация играет ключевую роль в корректной передаче параметров в кастомные декораторы.

export function MinLengthWithStep(min: number, step: number, options?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      name: "MinLengthWithStep",
      target: object.constructor,
      propertyName,
      options,
      constraints: [min, step],
      validator: {
        validate(value: string, args: ValidationArguments) {
          const [minLen, stepVal] = args.constraints;

          return (
            typeof value === "string" &&
            value.length >= minLen &&
            value.length % stepVal === 0
          );
        }
      }
    });
  };
}

Использование явных типов для параметров предотвращает ошибки при масштабировании библиотеки декораторов.


Динамическая интерпретация параметров

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

validate(value: any, args: ValidationArguments) {
  const [getLimit] = args.constraints;

  const lim it = typeof getLimit === "function"
    ? getLimit(args.object)
    : getLimit;

  return value <= limit;
}

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


Порядок обработки параметров в цепочке валидации

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

Каждый валидатор изолирован:

  • собственные constraints
  • собственный ValidationArguments
  • независимая логика validate

Расширенные сценарии передачи параметров

В сложных системах валидации параметры могут использоваться для:

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

Пример условного поведения:

export function IsGreaterThan(field: string, options?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      name: "IsGreaterThan",
      target: object.constructor,
      propertyName,
      options,
      constraints: [field],
      validator: {
        validate(value: number, args: ValidationArguments) {
          const relatedValue = (args.object as any)[args.constraints[0]];
          return typeof value === "number" && value > relatedValue;
        }
      }
    });
  };
}

Здесь параметр field определяет динамическую зависимость между свойствами объекта.


Итоговая модель передачи параметров

Внутренняя архитектура кастомных декораторов в Class-validator опирается на три основных механизма:

  • фабрика декоратора для захвата параметров через замыкание
  • constraints для структурированной передачи значений в валидатор
  • ValidationArguments для доступа к контексту выполнения

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