Библиотека js-joda построена на принципах неизменяемости и строгой
типизации временных сущностей, тогда как встроенный Date в
JavaScript является мутируемым объектом с неявной зоной ответственности
за форматирование, парсинг и арифметику. Это фундаментальное различие
определяет всю модель совместимости с legacy-кодом.
Основная точка соприкосновения — преобразование между
Date и Instant.
import { Instant } from '@js-joda/core';
// Date -> Instant
const instant = Instant.ofEpochMilli(date.getTime());
// Instant -> Date
const date = new Date(instant.toEpochMilli());
Использование epoch milliseconds выступает универсальным мостом между двумя моделями времени. Такой подход сохраняет корректность при работе с UTC и избегает локальных смещений, которые часто возникают при использовании строковых представлений.
Date изменяем через методы вроде setHours,
setDate, что делает его непредсказуемым в асинхронных
сценариях и при передаче по ссылке. В отличие от этого, все типы в
js-joda (LocalDate, LocalDateTime,
Instant, ZonedDateTime) являются
неизменяемыми.
При интеграции с legacy-кодом критично изолировать участки преобразования:
function legacyProcess(date) {
const instant = Instant.ofEpochMilli(date.getTime());
const processed = instant.plusSeconds(3600);
return new Date(processed.toEpochMilli());
}
Такая схема предотвращает распространение мутабельных объектов в доменную логику, сохраняя js-joda как слой строгих временных вычислений.
Legacy-системы часто используют JSON.stringify(Date),
что приводит к ISO-строке. js-joda использует явные методы
форматирования и парсинга.
const iso = instant.toString(); // ISO-8601
const parsed = Instant.parse(iso);
При интеграции с API, ожидающими стандарт ISO-8601, js-joda
демонстрирует более строгую и предсказуемую модель, чем
Date, который может вести себя по-разному в зависимости от
среды выполнения.
Проблема возникает при обратной совместимости:
const legacy = JSON.stringify({ date: new Date() });
// {"date":"2026-01-24T10:00:00.000Z"}
Такой формат требует явного контроля при десериализации:
const instant = Instant.parse(payload.date);
В старых кодовых базах часто встречается moment.js.
Совместимость реализуется через промежуточные преобразования в ISO или
epoch.
// moment -> js-joda
const instant = Instant.ofEpochMilli(momentObj.valueOf());
// js-joda -> moment
const m = moment(instant.toEpochMilli());
При переходе важно учитывать, что moment использует мутабельную
модель и не разделяет строго Local и Zoned
контексты так же явно, как js-joda. Это приводит к потенциальной потере
семантики временной зоны при конвертации.
Legacy API часто оперируют строками с неявной зоной
("2024-01-01 10:00"), что приводит к неоднозначности.
js-joda требует явного указания зоны через ZoneId.
import { ZonedDateTime, ZoneId } from '@js-joda/core';
const zdt = ZonedDateTime.of(
LocalDateTime.parse('2024-01-01T10:00'),
ZoneId.of('Europe/Moscow')
);
При взаимодействии с legacy-данными зона часто отсутствует, и тогда используется стратегия локализации:
const instant = Instant.ofEpochMilli(Date.parse(legacyString));
const zdt = instant.atZone(ZoneId.systemDefault());
Такой подход фиксирует неоднозначность на границе системы, не распространяя её внутрь доменной модели.
Legacy-код часто использует числовые диапазоны времени:
js-joda заменяет это строгими типами Duration и
Period.
import { Duration } from '@js-joda/core';
const updated = instant.plus(Duration.ofHours(2));
Совместимость обеспечивается через перевод:
const duration = Duration.ofMillis(msDiff);
const ms = duration.toMillis();
При миграции важно исключить смешивание арифметики
Date.getTime() и js-joda-операций внутри одного слоя
бизнес-логики, так как это приводит к дублированию правил округления и
ошибкам при DST-переходах.
В большинстве legacy API граница обмена представлена DTO-объектами со строковыми датами.
// входящий DTO
{
createdAt: "2026-05-25T12:30:00Z"
}
Преобразование в доменную модель:
const createdAt = Instant.parse(dto.createdAt);
Обратное преобразование:
const dto = {
createdAt: instant.toString()
};
Такой подход формирует устойчивый контракт: внешняя система всегда работает со строками, внутренняя — с типизированными временными объектами.
Несмотря на переход на js-joda, Date сохраняет роль
транспортного типа:
setTimeout,
Date.now())В таких случаях используется минимальный слой адаптации:
const nowInstant = Instant.ofEpochMilli(Date.now());
const legacyDate = new Date(nowInstant.toEpochMilli());
Изоляция Date в инфраструктурном слое позволяет избежать
проникновения его ограничений в бизнес-логику.
При переходе между моделями времени возникают типовые потери:
LocalDateTime → Date теряет контекст
зоныZonedDateTime → epoch фиксирует момент, но не сохраняет
исходное представлениеDate → LocalDate требует явного выбора
зоныconst local = instant.atZone(zone).toLocalDate();
Эти преобразования должны рассматриваться как осознанное упрощение модели, а не как нейтральная операция.
В больших legacy-системах применяется поэтапная замена:
Пример адаптера:
export const TimeAdapter = {
toInstant(date) {
return Instant.ofEpochMilli(date.getTime());
},
fromInstant(instant) {
return new Date(instant.toEpochMilli());
}
};
Такая структура минимизирует разрастание конвертационного кода по проекту и фиксирует единый контракт взаимодействия с legacy-слоем.