Переход с date-fns

date-fns и js-joda решают задачу работы с датами в JavaScript, но делают это на принципиально разных уровнях абстракции. При переходе между ними меняется не набор функций, а сама модель мышления о времени: от набора утилитарных операций к строгой объектной модели, основанной на спецификации ISO-8601 и концепциях из Java Time API.

date-fns построена как набор чистых функций, каждая из которых принимает Date и возвращает новый Date или строку. Основной акцент — функциональные операции:

  • addDays(date, amount)
  • format(date, pattern)
  • isBefore(dateLeft, dateRight)

js-joda, напротив, реализует объектную модель времени, где дата и время представлены специализированными типами:

  • LocalDate
  • LocalTime
  • LocalDateTime
  • ZonedDateTime
  • Instant

Каждый тип отражает конкретную семантику времени и исключает неоднозначность, характерную для Date.

Ключевое отличие заключается в том, что js-joda не работает с мутируемыми или частично определёнными датами: каждый объект строго определён по контексту.


Модель данных: Date против неизменяемых временных типов

В date-fns базовой сущностью остаётся встроенный Date. Он:

  • содержит локальное время и UTC одновременно
  • подвержен изменяемому состоянию (через методы setX)
  • не разделяет понятия «дата без времени» и «время без даты»

js-joda вводит строгую типизацию времени:

import { LocalDate } from '@js-joda/core';

const date = LocalDate.of(2026, 5, 25);

Здесь невозможно случайно получить смещение времени или скрытую зону — тип уже фиксирует смысл значения.


Форматирование и парсинг: переход от шаблонов к форматерам

В date-fns форматирование выглядит так:

import { format } from 'date-fns';

format(new Date(), 'yyyy-MM-dd');

В js-joda форматирование отделено от объекта даты:

import { LocalDate, DateTimeFormatter } from '@js-joda/core';

const date = LocalDate.of(2026, 5, 25);
const formatter = DateTimeFormatter.ofPattern('yyyy-MM-dd');

date.format(formatter);

Ключевое изменение

Форматтер становится объектом, а не строковым аргументом функции. Это уменьшает неоднозначность и повышает переиспользуемость форматов.


Операции сложения и вычитания времени

В date-fns операции выполняются через функции:

import { addDays, subMonths } from 'date-fns';

const next = addDays(new Date(), 5);
const prev = subMonths(new Date(), 2);

В js-joda операции становятся методами неизменяемых объектов:

import { LocalDate } from '@js-joda/core';

const date = LocalDate.of(2026, 5, 25);

const next = date.plusDays(5);
const prev = date.minusMonths(2);

Существенное отличие

Каждая операция возвращает новый экземпляр, а исходный объект остаётся неизменным. Это устраняет целый класс ошибок, связанных с побочными эффектами.


Сравнение дат: переход к строгой семантике

В date-fns:

import { isBefore } from 'date-fns';

isBefore(dateA, dateB);

В js-joda:

dateA.isBefore(dateB);

Здесь логика перемещается внутрь объекта, а сравнение становится частью доменной модели времени.


Работа с временем и зонами

date-fns требует отдельного пакета date-fns-tz для работы с часовыми поясами:

import { utcToZonedTime } from 'date-fns-tz';

js-joda решает проблему через отдельные типы:

import { ZonedDateTime, ZoneId } from '@js-joda/core';

const zone = ZoneId.of('Europe/Berlin');
const zdt = ZonedDateTime.now(zone);

Ключевая разница модели

  • date-fns: преобразование функций между форматами
  • js-joda: явное моделирование времени в конкретной зоне

Маппинг концепций date-fns на js-joda

1. Получение текущей даты

date-fns:

new Date()

js-joda:

import { LocalDate } from '@js-joda/core';

LocalDate.now();

2. Начало дня

date-fns:

import { startOfDay } from 'date-fns';

startOfDay(date);

js-joda:

date.atStartOfDay();

или через LocalDateTime:

date.atTime(0, 0);

3. Парсинг ISO

date-fns:

import { parseISO } from 'date-fns';

parseISO('2026-05-25');

js-joda:

LocalDate.parse('2026-05-25');

Миграция логики вычислений дат

При переходе важно учитывать, что js-joda разделяет уровни времени.

date-fns (смешанный подход):

import { addDays } from 'date-fns';

const result = addDays(new Date(), 10);

js-joda (строгая модель):

import { LocalDate } from '@js-joda/core';

const result = LocalDate.now().plusDays(10);

Если ранее использовались Date, необходимо определить, какая сущность нужна:

  • только дата → LocalDate
  • дата и время → LocalDateTime
  • дата и зона → ZonedDateTime
  • момент времени → Instant

Типовые ошибки при прямой замене

Ошибка 1: потеря временной зоны

date-fns:

new Date('2026-05-25')

js-joda требует явного типа:

LocalDate.parse('2026-05-25');

Если использовать Instant, потребуется временная зона:

import { Instant, ZoneId } from '@js-joda/core';

const instant = Instant.now();
const zoned = instant.atZone(ZoneId.of('Asia/Almaty'));

Ошибка 2: попытка использовать Date API

В js-joda отсутствуют методы:

  • getTime
  • setHours
  • getDate

Аналогичные операции заменяются преобразованиями типов:

date.withDayOfMonth(10);
date.plusWeeks(1);

Работа с диапазонами дат

В date-fns часто используются проверки:

isWithinInterval(date, { start, end });

В js-joda логика строится через сравнение:

date.isAfter(start) && date.isBefore(end);

или через включающие границы:

!date.isBefore(start) && !date.isAfter(end);

Структурирование временной логики в домене

js-joda способствует переносу логики в слой типов:

  • вычисления становятся методами объектов
  • исчезает необходимость в наборе утилитарных функций
  • уменьшается количество преобразований Date → string → Date

Пример доменной операции:

const nextBillingDate = billingDate.plusMonths(1);

В date-fns это выглядело бы как:

addMonths(billingDate, 1);

Обработка времени в асинхронных сценариях

При работе с API и серверными данными различие становится более заметным.

date-fns обычно используется после получения данных:

format(new Date(apiDate), 'dd.MM.yyyy');

js-joda требует явного преобразования входных данных:

const date = LocalDate.parse(apiDate);
date.format(DateTimeFormatter.ofPattern('dd.MM.yyyy'));

Преобразование Date → js-joda

Промежуточный этап миграции почти всегда требует адаптера:

import { LocalDate } from '@js-joda/core';

function fromDate(date) {
  return LocalDate.of(
    date.getFullYear(),
    date.getMonth() + 1,
    date.getDate()
  );
}

Альтернативный подход — переход через ISO:

LocalDate.parse(date.toISOString().substring(0, 10));

Организация слоя времени в проекте

При замене date-fns на js-joda часто выделяется отдельный слой:

  • date-utils (старый слой date-fns)
  • time-domain (новый слой js-joda)

Основная цель — изолировать:

  • парсинг входных данных
  • форматирование вывода
  • преобразования между типами

Изменение мышления при работе с датами

Переход с date-fns на js-joda меняет характер кода:

  • уменьшается количество «одноразовых» функций
  • исчезают скрытые преобразования Date
  • появляется явная модель времени
  • повышается предсказуемость операций

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