Временные зоны и форматы

Работа с датами в JavaScript традиционно опирается на объект Date, который хранит время в виде количества миллисекунд с эпохи Unix и внутренне использует UTC-представление. Это приводит к ключевой особенности: таймзона не хранится явно, а вывод и парсинг могут давать разные результаты в зависимости от окружения выполнения.

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

Базовая модель времени в Zod

Zod предоставляет несколько инструментов для работы с датами:

  • z.date() — проверка, что значение является экземпляром Date
  • z.coerce.date() — преобразование строки или числа в Date
  • z.string().datetime() — проверка ISO 8601 строки

Каждый из этих подходов по-разному взаимодействует с временными зонами.

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

ISO 8601 является основным стандартом для передачи даты и времени:

2026-05-10T12:30:00Z
2026-05-10T12:30:00+06:00
2026-05-10T12:30:00.000+03:00

В Zod доступна строгая проверка:

import { z } from "zod";

const schema = z.string().datetime();

По умолчанию допускается формат с Z (UTC), но не гарантируется наличие смещения.

Учет смещения таймзоны

Для работы с глобальными системами важно различать:

  • Z (UTC)
  • смещение +03:00, -06:00
  • локальное время без смещения

Zod позволяет явно требовать наличие offset:

const schema = z.string().datetime({ offset: true });

Это критично для систем, где локальное время без таймзоны считается ошибочным состоянием.

Проблема потери таймзоны в JavaScript Date

Объект Date не хранит информацию о временной зоне исходной строки. Например:

new Date("2026-05-10T12:00:00+06:00")

После парсинга:

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

Zod при использовании z.coerce.date() не сохраняет информацию о таймзоне:

const schema = z.coerce.date();

schema.parse("2026-05-10T12:00:00+06:00");

Результат — Date, нормализованный к UTC.

Стратегии работы с таймзонами

1. Хранение в UTC

Наиболее распространённая модель:

  • вход: ISO 8601 с offset
  • преобразование: Date
  • хранение: UTC
const schema = z.coerce.date();

Эта модель упрощает вычисления, сортировку и сравнение.

2. Сохранение оригинального смещения

Иногда требуется сохранить исходный offset как часть данных:

const schema = z.object({
  datetime: z.string().datetime({ offset: true }),
  offset: z.string().regex(/^[+-]\d{2}:\d{2}$/)
});

Здесь время и смещение разделяются, поскольку Date не подходит для хранения исходного контекста.

3. Использование IANA временных зон

Помимо offset существует система именованных зон:

  • Europe/Berlin
  • Asia/Almaty
  • America/New_York

Zod не предоставляет встроенной поддержки IANA таймзон, но позволяет валидировать их:

const tzSchema = z.string().refine((val) => {
  try {
    Intl.DateTimeFormat(undefined, { timeZone: val });
    return true;
  } catch {
    return false;
  }
});

Такая проверка опирается на Intl.

Разделение даты, времени и зоны

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

{
  "date": "2026-05-10",
  "time": "12:30",
  "timezone": "Asia/Almaty"
}

Схема Zod:

const schema = z.object({
  date: z.string(),
  time: z.string(),
  timezone: z.string()
});

Дальнейшая сборка выполняется отдельно, поскольку Zod не занимается интерпретацией календарной логики.

Преобразование через preprocess

z.preprocess позволяет нормализовать входные данные до валидации:

const schema = z.preprocess((val) => {
  if (typeof val === "string") {
    return new Date(val);
  }
  return val;
}, z.date());

Для таймзонных строк это основной механизм:

const schema = z.preprocess((val) => {
  if (typeof val !== "string") return val;
  return new Date(val); // учитывает offset
}, z.date());

Однако даже здесь сохраняется ограничение: исходный offset теряется после преобразования.

Форматы, требующие дополнительной обработки

RFC 3339 и ISO 8601 расширения

Некоторые системы используют расширенные форматы:

  • 2026-05-10T12:30:00+06:00[Asia/Almaty]
  • 2026-05-10T12:30:00Z/Europe/Berlin

Zod не распознаёт такие структуры напрямую. Они требуют ручного парсинга:

const schema = z.string().refine((val) => {
  return /^.+(Z|[+-]\d{2}:\d{2})$/.test(val);
});

или более строгой обработки через разбор строки.

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

date-fns-tz

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

import { zonedTimeToUtc } from "date-fns-tz";

const schema = z.preprocess((val) => {
  if (typeof val !== "string") return val;

  return zonedTimeToUtc(val, "Asia/Almaty");
}, z.date());

Luxon

Поддерживает полноценную модель DateTime:

import { DateTime } from "luxon";

const schema = z.preprocess((val) => {
  return DateTime.fromISO(val).toJSDate();
}, z.date());

Здесь сохраняется логика таймзоны на этапе парсинга, но итоговое значение всё равно становится Date.

Валидация временных диапазонов

Часто требуется проверка диапазонов времени с учётом таймзон:

const schema = z.object({
  start: z.date(),
  end: z.date()
}).refine((data) => {
  return data.end > data.start;
});

Такая проверка выполняется уже после нормализации всех значений в UTC.

Проблема неоднозначного локального времени

Существуют даты, которые неоднозначны:

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

Например:

2026-10-25 02:30 Europe/Berlin

Это время может существовать дважды или не существовать вовсе. Zod не решает эту проблему — она переносится в слой парсинга.

Стратегия строгой типизации временных данных

Для сложных систем применяется разделение типов:

  • ISODateTimeString — строка с offset
  • UTCDate — нормализованный Date
  • TimeZoneId — строка IANA
  • LocalDateTime — строка без зоны

Пример:

const ISODateTimeString = z.string().datetime({ offset: true });

const EventSchema = z.object({
  start: ISODateTimeString,
  timezone: z.string()
});

Ограничения Zod в контексте временных зон

Zod выполняет только:

  • структурную проверку
  • базовую валидацию формата
  • преобразование типов через preprocess/coerce

Он не предоставляет:

  • календарной логики
  • корректной работы с DST
  • интерпретации IANA зон
  • хранения временного контекста

Это переносится в специализированные библиотеки.

Нормализация временных данных как отдельный слой

В архитектуре приложений выделяется отдельный этап:

  1. Валидация Zod
  2. Преобразование в Date/UTC
  3. Применение временных зон
  4. Бизнес-логика

Zod выполняет только первый этап, обеспечивая согласованность входных данных, но не интерпретацию временного контекста.