Строгая типизация форматов

В большинстве сценариев работы с датами в JavaScript форматирование и парсинг опираются на строковые шаблоны. В Luxon это выражается через методы DateTime.toFormat и DateTime.fromFormat, где формат задаётся строкой с набором токенов (yyyy, MM, dd, HH:mm и т.д.).

Такой подход создаёт фундаментальную проблему: формат является обычной строкой, не проверяемой на этапе компиляции. Любая ошибка в токене проявляется только во время выполнения, а результатом становится некорректная дата или объект Invalid DateTime.

import { DateTime } from "luxon";

const dt = DateTime.now();

// Ошибка в формате: "mm" вместо "MM" для месяца
const result = dt.toFormat("yyyy-mm-dd");

Подобные ошибки не выявляются TypeScript’ом без дополнительных механизмов, поскольку тип метода принимает string, а не ограниченный набор допустимых форматов.


Поведение Luxon при некорректных форматах

Luxon не выбрасывает исключения при ошибках формата. Вместо этого используется модель “валидного/невалидного” объекта.

const parsed = DateTime.fromFormat("2024-13-40", "yyyy-MM-dd");

parsed.isValid; // false
parsed.invalidReason; // "unparsable"

При несовпадении строки и шаблона результат становится невалидным, что переносит контроль ошибок из стадии компиляции в стадию исполнения.


Структура форматов и источники неоднозначности

Форматы Luxon основаны на токенах Unicode Date Format Patterns. Основные группы:

  • Год: yyyy, yy
  • Месяц: MM, MMM, MMMM
  • День: dd
  • Время: HH, mm, ss
  • Таймзона: ZZ, ZZZ
DateTime.now().toFormat("yyyy LLL dd HH:mm");

Проблема возникает из-за отсутствия строгой типизации комбинаций этих токенов. Строка остаётся свободной формой записи, допускающей:

  • опечатки (yyy вместо yyyy)
  • логические ошибки (mm вместо MM)
  • несовместимые комбинации
  • неоднозначные локализации

Типобезопасность через TypeScript и её ограничения

TypeScript способен частично ограничить строковые форматы через literal types, однако Luxon не предоставляет встроенного типа, описывающего все допустимые шаблоны.

type DateFormat = "yyyy-MM-dd" | "dd.MM.yyyy";

function formatDate(dt: DateTime, format: DateFormat) {
  return dt.toFormat(format);
}

Такой подход вводит контроль, но масштабируется плохо:

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

Централизованный реестр форматов

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

export const DateFormats = {
  ISO_DATE: "yyyy-MM-dd",
  FULL_DATE: "dd.MM.yyyy",
  TIME: "HH:mm",
} as const;

export type DateFormat = typeof DateFormats[keyof typeof DateFormats];

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

function format(dt: DateTime, format: DateFormat) {
  return dt.toFormat(format);
}

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

  • единый источник истины
  • синхронизацию типов и значений
  • снижение риска несоответствий

Брендированные типы для форматов

Более строгая модель вводит “номинальную типизацию” для строк формата.

type FormatString = string & { __brand: "format" };

const asFormat = (s: string) => s as FormatString;

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

const ISO_DATE = asFormat("yyyy-MM-dd");

function parse(dt: string, format: FormatString) {
  return DateTime.fromFormat(dt, format);
}

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


Обёртки над Luxon для строгого интерфейса

Изоляция API Luxon позволяет ограничить прямое использование строковых форматов и централизовать их обработку.

function safeFromFormat(value: string, format: FormatString) {
  const dt = DateTime.fromFormat(value, format);

  if (!dt.isValid) {
    throw new Error(dt.invalidReason ?? "Invalid date");
  }

  return dt;
}

В таком случае некорректные данные обрабатываются сразу на границе системы.


Строгость при форматировании и парсинге

Luxon различает два направления работы:

  • toFormat — преобразование объекта даты в строку
  • fromFormat — создание даты из строки

Оба метода используют один и тот же механизм токенов, но ошибки проявляются по-разному:

  • при toFormat ошибки связаны с неправильными токенами
  • при fromFormat ошибки связаны с несоответствием строки шаблону
DateTime.fromFormat("31/02/2024", "dd/MM/yyyy"); // invalid

Строгая типизация здесь направлена не на сами даты, а на форматные контракты между слоями системы.


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

Luxon предоставляет предопределённые форматы через toLocaleString, что снижает необходимость строковых шаблонов.

DateTime.now().toLocaleString(DateTime.DATE_SHORT);

Примеры стандартных форматов:

  • DateTime.DATE_SHORT
  • DateTime.DATE_MED
  • DateTime.DATE_FULL
  • DateTime.DATETIME_MED

Эти значения являются более безопасной альтернативой строковым форматам, поскольку:

  • не требуют ручного ввода токенов
  • поддерживаются через enum-like структуру
  • интегрируются с локалями

Локализация и влияние на типизацию форматов

Форматы Luxon тесно связаны с локалями, что добавляет дополнительный уровень вариативности.

DateTime.now().setLocale("ru").toLocaleString(DateTime.DATE_FULL);

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


Слабые места строгой типизации форматов

Даже при использовании TypeScript и централизованных констант остаются ограничения:

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

Контрактный подход к форматам

Наиболее устойчивой моделью становится трактовка формата как контракта между слоями:

  • слой хранения данных
  • слой бизнес-логики
  • слой отображения

Формат перестаёт быть произвольной строкой и становится идентификатором правила преобразования.

type FormatKey = "ISO" | "UI_DATE" | "API_TIMESTAMP";

const formatMap: Record<FormatKey, string> = {
  ISO: "yyyy-MM-dd",
  UI_DATE: "dd.MM.yyyy",
  API_TIMESTAMP: "yyyy-MM-dd'T'HH:mm:ss",
};

Интеграция строгих форматов в архитектуру

При масштабировании приложения форматы перестают быть локальной деталью и переходят в инфраструктурный слой.

Характерные признаки такого подхода:

  • запрет прямых строковых форматов вне модуля дат
  • единый модуль форматирования
  • централизованные правила парсинга
  • строгие ключи вместо строковых шаблонов
function formatDate(dt: DateTime, key: FormatKey) {
  return dt.toFormat(formatMap[key]);
}

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