Валидация входных данных

Валидация на границе системы предотвращает ошибки, некорректный вывод и потенциальные проблемы безопасности. Особенно важна при работе с данными из API, пользовательского ввода или localStorage.


Что нужно валидировать

  • Тип входных данных (Date, string, number).
  • Корректность числового значения (не NaN, не Infinity).
  • Допустимый диапазон даты.
  • Формат строки (ISO 8601 предпочтителен).
  • Наличие значения (не null, не undefined).

Базовая функция валидации

type DateInput = Date | string | number;
type ValidationResult = { valid: true; date: Date } | { valid: false; reason: string };

function validateDateInput(input: unknown): ValidationResult {
  if (input === null || input === undefined) {
    return { valid: false, reason: 'Value is null or undefined' };
  }

  if (typeof input !== 'string' && typeof input !== 'number' && !(input instanceof Date)) {
    return { valid: false, reason: `Unexpected type: ${typeof input}` };
  }

  if (typeof input === 'number' && !isFinite(input)) {
    return { valid: false, reason: `Non-finite number: ${input}` };
  }

  const d = new Date(input as any);

  if (isNaN(d.getTime())) {
    return { valid: false, reason: `Cannot parse date: "${input}"` };
  }

  return { valid: true, date: d };
}

Валидация диапазона

const MIN_DATE = new Date('2000-01-01T00:00:00Z').getTime();
const MAX_DATE = () => Date.now() + 365 * 86400_000; // Не более года в будущем

function validateDateRange(d: Date): string | null {
  const ts = d.getTime();

  if (ts < MIN_DATE) {
    return `Date is too far in the past: ${d.toISOString()}`;
  }

  if (ts > MAX_DATE()) {
    return `Date is too far in the future: ${d.toISOString()}`;
  }

  return null; // Ошибок нет
}

Строгая обёртка format с валидацией

import { format } from 'timeago.js';

function safeFormat(input: unknown, locale = 'ru'): string {
  const validation = validateDateInput(input);

  if (!validation.valid) {
    if (process.env.NODE_ENV !== 'production') {
      console.warn(`[timeago] Invalid date input: ${validation.reason}`);
    }
    return '';
  }

  const rangeError = validateDateRange(validation.date);
  if (rangeError) {
    if (process.env.NODE_ENV !== 'production') {
      console.warn(`[timeago] Date out of range: ${rangeError}`);
    }
  }

  return format(validation.date, locale);
}

Валидация ISO 8601 строк

const ISO_8601_REGEX = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d{1,3})?)?(Z|[+\-]\d{2}:\d{2})?)?$/;

function isISO8601(str: string): boolean {
  return ISO_8601_REGEX.test(str);
}

function validateISOString(input: unknown): ValidationResult {
  if (typeof input !== 'string') {
    return { valid: false, reason: 'Not a string' };
  }

  if (!isISO8601(input)) {
    return { valid: false, reason: `Not ISO 8601 format: "${input}"` };
  }

  const d = new Date(input);
  if (isNaN(d.getTime())) {
    return { valid: false, reason: `ISO string parses to invalid date: "${input}"` };
  }

  return { valid: true, date: d };
}

Валидация timestamp

function validateTimestamp(ts: unknown): ValidationResult {
  if (typeof ts !== 'number') {
    return { valid: false, reason: `Not a number: ${typeof ts}` };
  }

  if (!isFinite(ts)) {
    return { valid: false, reason: `Non-finite value: ${ts}` };
  }

  // Timestamp в секундах (Unix)? Конвертировать
  const adjustedTs = ts < 1e10 ? ts * 1000 : ts;

  const d = new Date(adjustedTs);
  if (isNaN(d.getTime())) {
    return { valid: false, reason: `Invalid timestamp: ${ts}` };
  }

  return { valid: true, date: d };
}

Zod схема для объектов с датами

import { z } from 'zod';

const PostSchema = z.object({
  id:        z.number(),
  title:     z.string(),
  createdAt: z.string().datetime(), // ISO 8601 с Z
});

type Post = z.infer<typeof PostSchema>;

function formatPostDate(post: Post, locale = 'ru'): string {
  // post.createdAt уже валидирован Zod — гарантированно ISO строка
  return format(post.createdAt, locale);
}

Валидация на границе API ответа

async function fetchPosts(): Promise<Post[]> {
  const response = await fetch('/api/posts');
  const data     = await response.json();

  // Валидировать перед использованием
  return data.map((item: unknown) => {
    const result = PostSchema.safeParse(item);

    if (!result.success) {
      console.error('Invalid post data:', result.error);
      return null;
    }

    return result.data;
  }).filter(Boolean);
}

React компонент с валидацией props

import { format } from 'timeago.js';

interface TimeAgoProps {
  date?:    Date | string | number;
  locale?:  string;
  fallback?: string;
}

function TimeAgo({ date, locale = 'ru', fallback = '' }: TimeAgoProps) {
  if (!date) return <>{fallback}</>;

  const d = new Date(date as any);
  if (isNaN(d.getTime())) return <>{fallback}</>;

  return <time dateTime={d.toISOString()}>{format(d, locale)}</time>;
}

// PropTypes для JavaScript проектов
TimeAgo.defaultProps = { locale: 'ru', fallback: '' };

Таблица источников данных и рисков

Источник Тип данных Риск Решение
Бэкенд API (доверенный) ISO строка Низкий validateDateInput
URL параметр Строка Высокий validateISO + range check
localStorage Строка Средний validateDateInput
Пользовательский ввод Произвольная строка Высокий Полная валидация
Внутренние данные Известный тип Минимальный Тип в TypeScript