В большинстве сценариев работы с датами в 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 не выбрасывает исключения при ошибках формата. Вместо этого используется модель “валидного/невалидного” объекта.
const parsed = DateTime.fromFormat("2024-13-40", "yyyy-MM-dd");
parsed.isValid; // false
parsed.invalidReason; // "unparsable"
При несовпадении строки и шаблона результат становится невалидным, что переносит контроль ошибок из стадии компиляции в стадию исполнения.
Форматы Luxon основаны на токенах Unicode Date Format Patterns. Основные группы:
yyyy, yyMM, MMM, MMMMddHH, mm, ssZZ, ZZZDateTime.now().toFormat("yyyy LLL dd HH:mm");
Проблема возникает из-за отсутствия строгой типизации комбинаций этих токенов. Строка остаётся свободной формой записи, допускающей:
yyy вместо yyyy)mm вместо MM)TypeScript способен частично ограничить строковые форматы через literal types, однако Luxon не предоставляет встроенного типа, описывающего все допустимые шаблоны.
type DateFormat = "yyyy-MM-dd" | "dd.MM.yyyy";
function formatDate(dt: DateTime, format: DateFormat) {
return dt.toFormat(format);
}
Такой подход вводит контроль, но масштабируется плохо:
Одним из устойчивых подходов становится выделение единого источника форматов, который используется как на уровне типов, так и на уровне исполнения.
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);
}
Этот подход не предотвращает ошибки в содержимом строки, но изолирует контекст использования форматов от произвольных строк.
Изоляция 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_SHORTDateTime.DATE_MEDDateTime.DATE_FULLDateTime.DATETIME_MEDЭти значения являются более безопасной альтернативой строковым форматам, поскольку:
Форматы 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]);
}
Такое разделение уменьшает количество ошибок, связанных с несоответствием форматов между компонентами системы.