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, напротив, реализует объектную модель времени, где дата и время представлены специализированными типами:
LocalDateLocalTimeLocalDateTimeZonedDateTimeInstantКаждый тип отражает конкретную семантику времени и исключает
неоднозначность, характерную для Date.
Ключевое отличие заключается в том, что js-joda не работает с мутируемыми или частично определёнными датами: каждый объект строго определён по контексту.
В date-fns базовой сущностью остаётся встроенный Date.
Он:
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:
new Date()
js-joda:
import { LocalDate } from '@js-joda/core';
LocalDate.now();
date-fns:
import { startOfDay } from 'date-fns';
startOfDay(date);
js-joda:
date.atStartOfDay();
или через LocalDateTime:
date.atTime(0, 0);
date-fns:
import { parseISO } from 'date-fns';
parseISO('2026-05-25');
js-joda:
LocalDate.parse('2026-05-25');
При переходе важно учитывать, что js-joda разделяет уровни времени.
import { addDays } from 'date-fns';
const result = addDays(new Date(), 10);
import { LocalDate } from '@js-joda/core';
const result = LocalDate.now().plusDays(10);
Если ранее использовались Date, необходимо определить,
какая сущность нужна:
LocalDateLocalDateTimeZonedDateTimeInstantdate-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'));
В js-joda отсутствуют методы:
getTimesetHoursgetDateАналогичные операции заменяются преобразованиями типов:
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'));
Промежуточный этап миграции почти всегда требует адаптера:
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Каждая операция становится частью типизированной системы, где ошибка чаще выявляется на уровне архитектуры, а не выполнения.