Валидация дат

Работа с датами в JavaScript связана с рядом особенностей: неявное приведение типов, различия между строковым и объектным представлением времени, влияние временных зон и неоднозначность форматов. В контексте серверных приложений и DTO-валидации требуется строгий контроль входных значений, где библиотека class-validator предоставляет набор инструментов для проверки и ограничения дат.

Представление даты в JavaScript и источники проблем

В JavaScript дата может существовать в нескольких формах:

  • Date-объект
  • строка (ISO 8601, произвольные форматы)
  • числовое значение (timestamp)

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

  • "2026-01-01" — валидная ISO-строка, но интерпретация зависит от контекста
  • "01/02/2026" — неоднозначный формат (день/месяц или месяц/день)
  • Date может быть Invalid Date, не вызывая явной ошибки при создании

Базовая проверка типа даты

В class-validator основной инструмент для проверки объекта Date:

import { IsDate } from 'class-validator';

export class CreateEventDto {
  @IsDate()
  startDate: Date;
}

Декоратор @IsDate() проверяет, что значение является экземпляром Date и при этом не является Invalid Date.

Особенность: строковые значения автоматически не преобразуются в Date. Без предварительной трансформации валидация не пройдет.

Преобразование строк в Date через class-transformer

Для корректной работы с входными JSON-данными часто применяется связка с class-transformer:

import { Type } from 'class-transformer';
import { IsDate } from 'class-validator';

export class CreateEventDto {
  @Type(() => Date)
  @IsDate()
  startDate: Date;
}

Декоратор @Type(() => Date) обеспечивает преобразование входной строки в объект Date до этапа валидации.

Типичная схема обработки входных данных:

  1. JSON → plain object
  2. преобразование типов (class-transformer)
  3. валидация (class-validator)

Проверка ISO 8601 строк

В случаях, когда дата приходит в виде строки, используется проверка формата ISO:

import { IsISO8601 } from 'class-validator';

export class CreateEventDto {
  @IsISO8601()
  startDate: string;
}

Особенности:

  • проверяется соответствие формату ISO 8601
  • значение остаётся строкой
  • не выполняется преобразование в Date

Допустимые примеры:

  • 2026-05-15T10:00:00Z
  • 2026-05-15
  • 2026-05-15T10:00:00+03:00

Недопустимые примеры:

  • 15-05-2026
  • 2026/05/15
  • May 15 2026

Ограничение минимальной даты

Для ограничения нижней границы используется @MinDate():

import { MinDate } from 'class-validator';

export class CreateEventDto {
  @Type(() => Date)
  @MinDate(new Date('2026-01-01'))
  startDate: Date;
}

Поведение:

  • значение должно быть строго больше или равно указанной даты
  • сравнение выполняется через числовое представление времени (getTime())

Особенности применения:

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

Пример динамической границы:

@MinDate(new Date(Date.now()))
startDate: Date;

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

Ограничение максимальной даты

Для верхней границы применяется @MaxDate():

import { MaxDate } from 'class-validator';

export class CreateEventDto {
  @Type(() => Date)
  @MaxDate(new Date('2030-01-01'))
  startDate: Date;
}

Используется в сценариях:

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

Сравнение дат между полями

Сравнение двух дат внутри одной модели напрямую не поддерживается встроенными декораторами, поэтому применяется кастомная логика:

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

export function IsAfter(property: string) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      name: 'isAfter',
      target: object.constructor,
      propertyName,
      constraints: [property],
      validator: {
        validate(value: any, args: ValidationArguments) {
          const [relatedPropertyName] = args.constraints;
          const relatedValue = (args.object as any)[relatedPropertyName];

          if (!value || !relatedValue) return false;

          return new Date(value).getTime() > new Date(relatedValue).getTime();
        },
      },
    });
  };
}

Применение:

export class CreateEventDto {
  @Type(() => Date)
  startDate: Date;

  @Type(() => Date)
  @IsAfter('startDate')
  endDate: Date;
}

Валидация timestamp значений

Часто даты приходят в виде чисел (Unix timestamp):

import { IsNumber } from 'class-validator';

export class CreateEventDto {
  @IsNumber()
  startTimestamp: number;
}

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

startTimestamp > 0 && startTimestamp < 4102444800000

(ограничение примерно до 2100 года в миллисекундах)

Проверка валидности Date объекта

При преобразовании строк в Date возможна ситуация Invalid Date. Проверка через class-validator:

import { ValidateBy, ValidationOptions } from 'class-validator';

export function IsValidDate(validationOptions?: ValidationOptions) {
  return ValidateBy(
    {
      name: 'isValidDate',
      validator: {
        validate(value: any) {
          return value instanceof Date && !isNaN(value.getTime());
        },
      },
    },
    validationOptions,
  );
}

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

export class CreateEventDto {
  @Type(() => Date)
  @IsValidDate()
  startDate: Date;
}

Работа с временными зонами

Date в JavaScript всегда хранится в UTC, однако строковые представления могут содержать локальные смещения:

  • 2026-05-15T10:00:00Z — UTC
  • 2026-05-15T10:00:00+03:00 — смещение +3 часа

При валидации важно учитывать:

  • сравнение выполняется в миллисекундах UTC
  • локальная зона не сохраняется в объекте Date
  • возможны ошибки при сериализации/десериализации

Частые ошибки при валидации дат

Отсутствие трансформации

@IsDate()
startDate: Date;

При передаче строки в JSON валидация всегда будет провалена без @Type(() => Date).

Использование неоднозначных форматов

Строки вида 01-02-2026 интерпретируются по-разному в различных средах исполнения.

Сравнение строк вместо дат

if (dto.startDate < dto.endDate)

При строковом типе сравнение становится лексикографическим, а не временным.

Комбинирование валидаторов

Практическая схема для строгой проверки даты включает несколько уровней:

export class CreateEventDto {
  @Type(() => Date)
  @IsDate()
  @MinDate(new Date())
  @MaxDate(new Date('2030-01-01'))
  startDate: Date;
}

Такой подход обеспечивает:

  • корректный тип
  • допустимый диапазон
  • предотвращение некорректных значений времени

Использование в структурах DTO

В типичных серверных приложениях DTO с датами формируют основу контрактов между слоями:

  • входные данные API
  • параметры фильтрации
  • события и логирование времени

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