Всем привет! Меня зовут Дмитрий, и я занимаюсь веб-разработкой в IT-компании Intelsy, работая как на аутсорс-, так и на аутстафф-проектах. В своей работе я постоянно сталкиваюсь с задачами, связанными с датами и временем, и давно заметил, что стандартный объект Date в JavaScript часто доставляет много неудобств. Мне захотелось разобраться, почему так происходит и какие современные решения помогают упростить эту работу. Это привело меня к изучению нового API Temporal — перспективного инструмента для более точной и удобной работы с датой, временем и часовыми поясами.
Кому будет полезна эта статья:
опытным разработчикам, которые хотят глубже понять недостатки Date и познакомиться с новым подходом Temporal,
начинающим, чтобы быстрее разобраться в тонкостях работы с датами в JavaScript и избежать распространённых ошибок,
командам, разрабатывающим международные веб-приложения, для которых корректная работа с часовыми поясами и временем — ключ к стабильности и качеству продукта.
Что такое Date?
Для работы с датами в JavaScript разработчики используют Date – объект для работы со временем. Объект Date содержит число миллисекунд, прошедших с полуночи 1 января 1970 года (это также называется эпохой Unix).
JavaScript появился в 1995 году. Он разрабатывался быстро и многие решения создавались с учётом ограниченного количества времени и ресурсов. Как раз, когда потребовалось создать объект для работы с датами, Date был вдохновлён объектом Date из языка Java. Можно сказать, что Date был реализован как обёртка вокруг числа, представляющего количество миллисекунд с эпохи Unix.
И хоть со временем стали явными недостатки Date, его никто не менял, чтобы поддерживать старые веб-приложения. Для минимизации минусов Date появились такие решения, как moment.js, date-fns, dayjs, luxon и т.п. К тому же, в настоящее время ведётся разработка нового API Temporal, который призван решить многие проблемы Date.
Создание даты
С помощью Date мы можем создавать даты:
// Текущая дата и время // Конкретная дата (7 декабря 2025)
|
Создав объект Date, мы можем, используя его методы, получить нужный нам компонент времени:
const date = new Date(); |
Существуют также UTC-варианты методов, возвращающие день, месяц, год и т.д. для зоны UTC (всемирное координированное время): getUTCFullYear, getUTCMonth, getUTCDay. Если говорить простыми словами, UTC – это основное время на планете, от которого отсчитываются все другие часовые пояса.
Помимо получения времени, Date имеет методы и для его преобразования:
const date = new Date(); date.setFullYear(2024); date.setHours(12); date.setMinutes(30); date.setSeconds(30); date.setMilliseconds(50); date.setTime(100); |
Для этих методов также есть UTC-варианты, которые устанавливают время и дату в зоне UTC: setUTCFullYear, setUTCMonth, setUTCDate и т.д.
Преобразование через такие методы будет менять значение в объекте. То есть объект Date, сам по себе, является мутабельным и методы, изменяющие дату, не создают новый объект, а преобразуют имеющийся, что может быть не очень хорошо. Иммутабельность может быть полезна для предотвращения случайных изменений данных и упрощения работы с состоянием, в то время как мутабельность эти случайные изменения может породить.
Таким образом, для корректного преобразования дат через Date, например, добавление/вычитание пары дней или часов потребуется создание нового объекта даты.
const now = new Date();
|
Автокоррекция
У Date есть довольно интересный механизм – автокоррекция. Он заключается в том, что можно устанавливать числа, не существующие в реальных датах, а объект затем сам себя подкорректирует.
const date = new Date(2025, 1, 28); // 28 февраля 2025 |
Но такое поведение может привести к ошибкам. Как мы помним, месяц в Date нужно указывать с нуля. Допустим, не подразумевая этого, разработчик захочет в конструкторе указать декабрь месяц (12), но на выходе получит январь следующего года. Такую же ошибку можно совершить, указав в месяце большее число дней, чем в нём на самом деле.
const date = new Date(2024, 12, 1); |
В целом, автокоррекция, действительно, является полезной в сценариях, где нужно выполнить арифметические операции с датами. Например, добавить несколько дней или часов к текущей дате. Тем не менее, автокоррекция может стать и источником ошибок, если разработчик не учитывает её поведение и случайно указывает неверные числа в дате или если выбор даты работает на основе пользовательского ввода, где пользователь может вписать любые числа, тогда потребуется дополнительная проверка на корректность компонентов даты в коде.
Невалидная дата
Может возникнуть вопрос: а как быть, если установить невалидную дату, причём такую, что JavaScript не сможет её обработать? В таком случае Date вернёт значение Invalid Date. Такие значения следует заранее проверять, чтобы избежать ошибок при дальнейшем выполнении кода. Для невалидной даты методы получения времени будут возвращать значение NaN (ошибочное значение после неправильной операции с числами). Тогда проверить валидность даты можно таким образом:
const invalidDate = new Date('invalid date'); // Invalid Date |
Сравнение дат
Ко всему прочему, JavaScript позволяет сравнивать даты с помощью операторов <, <=, >, >=. Например, может пригодиться, когда мы хотим сделать сортировку по дате. Операторы сравнения работают, потому что при их использовании Date автоматически преобразуется в число (миллисекунды).
const date1 = new Date(2025, 0, 1); |
Операторы == и === в данном случае работать не будут, потому что Date не преобразуется в число при их использовании. Для сравнения дат на равенство можно использовать метод getTime для получения миллисекунд и их сравнения.
const date1 = new Date(2025, 0, 1); |
Форматирование даты
Если требуется преобразовать дату в красивый формат, использовать Date становится неудобно.
Справедливости ради, у Date есть методы toLocaleString, toLocaleDateString и toLocaleTimeString, которые упрощают работу с форматированием, но и имеют некоторые ограничения. Эти методы возвращают строковое представление даты и времени, отформатированное в соответствии с локальными настройками пользователя.
const date = new Date(2024, 9, 15); |
const date = new Date(2024, 9, 15, 14, 30, 30);
|
Хотя этот метод работает, он может быть недостаточно гибким для более сложных сценариев. Например, toLocaleString не предоставляет опций для работы с разделителями или не может поменять местами компоненты даты и времени. К тому же поведение этих методов может отличаться в разных браузерах. Например, формат даты и времени может быть разным в разных браузерах и даже версиях, несмотря на то что указана одна и та же локаль.
Часовые пояса
Одной из самых больших проблем при работе с объектом Date в JavaScript является его зависимость от локального времени пользователя. Date работает только с 2-мя часовыми зонами: локальным временем пользователя и временем UTC. Объект Date изначально не был предназначен для работы с часовыми поясами, что приводит к определённым ограничениям.
Создать дату в зоне UTC мы можем либо через строку, либо через метод Date.UTC.
// Z в конце строки означает UTC-время const date1 = new Date('2025-01-07T12:30:00Z');
|
Можно даже создать дату в определённом часовом поясе. Например, создадим дату по МСК (UTC+3), будучи в UTC+4. Однако увидим, что работать мы сможем только либо с локальным, либо с UTC-временем.
const date = new Date('2025-01-07T12:00:00+03:00'); // 12:00 (UTC+3) |
Для получения компонентов даты в определённом часовом поясе придётся вручную вычислять смещение. Для этого даже есть отдельный метод getTimezoneOffset, который возвращает смещение относительно UTC в минутах. Однако даже, если мы вручную вычислим смещение, то мы всё равно не получим полную информацию о часовом поясе, так как в нём может содержаться ещё летнее время (DST).
const getDateWithOffset = (date: Date, offset: number) => { |
На самом деле, если не требуется никакой работы с датой в определённом часовом поясе, но требуется только показать её на экран, то есть способ проще через toLocaleString, который поддерживает параметр timeZone.
// 12:30:00 const date = new Date(2025, 0, 1, 12, 30); // "04:30:00" |
Работа с часовыми поясами в Date является сложной и неудобной из-за отсутствия явной поддержки часовых поясов и ограниченного API. Конечно, с помощью методов UTC, ручного вычисления смещения можно решить определённые задачи, однако в сложных сценариях, например, DST, лучше справиться с этим помогут различные сторонние решения.
Итог
Работа с объектом Date в JavaScript имеет ряд недостатков, которые могут осложнить разработку, особенно в больших и сложных сценариях:
неудобное API – месяцы и дни недели нумеруются с нуля в отличие от других компонентов даты;
отсутствие поддержки часовых поясов;
мутабельность, которая может привести к случайным изменениям;
автокоррекция, позволяющая вводить несуществующую дату;
форматирование имеет ограничения.
Intl.DateTimeFormat
Intl.DateTimeFormat – это JavaScript объект, который предоставляет инструменты для форматирования дат и времени с учётом языка и региональных настроек пользователя. Он является частью стандарта ECMAScript Internationalization API, который был добавлен в JavaScript для поддержки интернационализации (i18n).
Intl.DateTimeFormat позволяет форматировать дату и время как вместе, так и по отдельности с учётом локали пользователя и разных настроек. При этом, не все настройки являются обязательными и некоторые можно пропустить (например, убрать часы, но оставить минуты и секунды).
const date = new Date(2025, 0, 5, 14, 30); // 5 января 2025, 14:30:00 |
Настройка Intl.DateTimeFormat похожа на метод toLocaleString с некоторым отличием в использовании: Intl.DateTimeFormat создаёт функцию-форматтер, которую затем можно вызывать с любой датой, а toLocaleString вызывается как метод уже имеющегося объекта Date и возвращает отформатированную строку. Но, даже несмотря на их схожесть, их реализация в браузерах может отличаться, так как нет строгой спецификации и каждый браузер может интерпретировать требования по-разному.
Intl.DateTimeFormat, сам по себе, является хорошим дополнением к Date, позволяющий без использования сторонних библиотек приводить даты ко многим форматам, хоть и ограничен теми моментами, о которых говорилось ранее.
Temporal
Temporal – это новый API для работы с датой и временем в JavaScript, который призван устранить многие недостатки объекта Date. На момент написания этой статьи, Temporal находится на стадии предложения (proposal) и ещё не включен в стандарт ECMAScript. Однако сейчас его можно попробовать через полифиллы (код, который добавляет функциональность, отсутствующую в некоторых браузерах или средах выполнения), пока не будет полноценной поддержки браузерами.
На данный момент есть 2 полифилла @js-temporal/polyfill – полифилл, размещённый одним из участников разработки Temporal, что может гарантировать соответствие текущей спецификации, и temporal-polyfill – полифилл от сторонних разработчиков, более легковесный и упрощённый. Хоть использование этих полифиллов почти ничем не отличается, а популярность у сообщества примерно одинаковая, я предпочту продемонстрировать в данной статье более официальный вариант. Но, в целом, основные объекты и методы в этих полифиллах совпадают.
npm install @js-temporal/polyfill |
В отличие от Date, Temporal состоит из нескольких объектов, каждый из которых имеет свою уникальную реализацию, свои методы и свойства и служит для решения определённых задач. Основные объекты Temporal:
Temporal.Now – текущая дата и время.
Temporal.Instant – какая-либо точка во времени.
Temporal.PlainDate – дата без времени и часового пояса.
Temporal.PlainTime – время без даты и часового пояса.
Temporal.PlainDateTime – дата и время без часового пояса.
Temporal.PlainYearMonth – год и месяц без даты и часового пояса.
Temporal.PlainMonthDate – месяц и дата без года и часового пояса.
Temporal.ZonedDateTime – дата и время с часовым поясом.
Temporal.Duration – продолжительность времени.
Temporal.Calendar – календарь.
Temporal.Now
Объект Temporal.Now предоставляет несколько методов и свойств, которые имеют информацию о текущем времени. В целом, этот объект нужен для перевода текущего времени в другие объекты Temporal.
Например, метод instant возвращает объект Temporal.Instant с текущим системным временем.
const instant = Temporal.Now.instant(); |
Отличие, которое бросается в глаза, это то, что Temporal умеет возвращать id текущего часового пояса в методе timeZoneId, что более информативно, чем обычное смещение в Date. Затем этот id можно использовать для формирования даты в определённом часовом поясе, включая DST.
const timeZoneId = Temporal.Now.timeZoneId();
|
Также есть метод plainDate, который возвращает текущую дату как объект Temporal.PlainDate в исчислении определённого календаря. plainDateISO делает то же самое, только в фиксированном календаре ISO 8601 (стандартная адаптация григорианского календаря). При желании, можно указать часовой пояс, в котором вычисляется дата.
const instant = Temporal.Now.plainDate('iso8601'); const isoInstant = Temporal.Now.plainDateISO('Europe/Moscow'); |
Для работы с временем есть метод plainTimeISO, который возвращает текущее системное время в виде объекта Temporal.PlainTime в исчислении календаря ISO 8601 и часового пояса, который указывать необязательно.
const time = Temporal.Now.plainTimeISO('Europe/Moscow'); |
Похожие методы plainDateTime и plainDateTimeISO позволяют работать сразу как с датой, так и с временем через объект Temporal.PlainDateTime.
У Temporal.Now есть методы zonedDateTime и zonedDateTimeISO, которые возвращают объект Temporal.ZonedDateTime и очень похожи по структуре и возвращаемым параметрам на Temporal.PlainDateTime, только отличие в том, что через эти методы можно также получить смещение относительно UTC и время с момента Unix-эпохи.
const date = Temporal.Now.zonedDateTime('iso8601', 'Europe/Moscow'); |
Temporal.Instant
Temporal.Instant представляет собой точку во времени с точностью до наносекунды. Этот объект похож на тот, который мы получили в Temporal.Now.instant(), только, на этот раз, мы можем задать необходимое нам время через метод.
const date = Temporal.Instant.fromEpochMilliseconds(Date.now()); |
Можно сказать, что объект Temporal.Instant – аналог привычного new Date(). Отличие в том, что точность измерения времени у Temporal может достигать наносекунд, в отличие от миллисекунд Date. Такая высокая точность может быть очень полезна в сложных системах, завязанных на времени.
const now = Temporal.Now.instant();
console.log(now.epochSeconds); // 1 700 000 000 console.log(now.epochMicroseconds); // 1 700 000 000 000 000n |
Есть возможность создать точку во времени из строки в формате ISO 8601 с помощью метода from. К тому же Temporal.Instant даёт возможность перевести дату в часовой пояс, используя известные методы toZonedDateTime и toZonedDateTimeISO.
// 01.02.2025 12:30:30 UTC+4 |
Temporal.PlainDate
Temporal.PlainDate можно просто представить как дату без времени и без привязки к часовому поясу. Создать такую дату можно через конструктор или метод from, принимающий строку, или объект Temporal.PlainDate.
// 05.01.2025 console.log(date.calendarId); // iso8601 console.log(date.inLeapYear); // Високосный год (true/false) |
Уже заметно, что день недели (dayOfWeek) и месяц (month) имеют удобный диапазон значений в отличие от Date. Также появилось поле dayOfYear для получения дня в году и много других новых полей.
Например, у некоторых календарей есть так называемые эры, и Temporal учитывает этот момент. Так можно узнать, что по буддийскому календарю сейчас эра be, год по эре – 2568, а в остальном всё совпадает с нашим календарём.
const date = Temporal.Now.plainDate('buddhist', 'Europe/Moscow');
console.log(date.year); // 2568 |
Полученный объект затем можно как привязать к часовому поясу через уже известный метод toZonedDateTime, так и расширить, добавив время методом toPlainDateTime.
// 05.01.2025 |
Temporal.PlainTime
Temporal.PlainTime – это просто время без привязки к часовому поясу или какой-либо дате. Создать время можно также через конструктор или метод from.
// 12:30:30 console.log(time.hour); // 0-23 |
Полученное время можно также привязать к часовому поясу или объединить с датой.
// 12:30:30 |
Temporal.PlainDateTime
Temporal.PlainDateTime объединяет в себе Temporal.PlainDate и Temporal.PlainTime и представляет собой дату и время без привязки к часовому поясу. Его методы и свойства аналогичны тому, что мы видели в Temporal.PlainDate и Temporal.PlainTime.
// 05.01.2025 12:30:30
|
Объект Temporal.PlainDateTime можно довольно просто перевести в обычную дату или время, а также привязать к ним часовой пояс.
// 05.01.2025 12:30:30 |
Temporal.ZonedDateTime
Этот объект представляет из себя тот же Temporal.PlainDateTime, совмещённый с Temporal.Instant, и с привязкой к часовому поясу, который необходимо будет указать при создании. Создание даты через конструктор требует время в наносекундах, поэтому без Temporal.Instant не обойтись.
const now = Temporal.Now.instant();
|
Так как в Temporal.ZonedDateTime дата может представлять из себя точку во времени, то её можно перевести в Temporal.Instant методом toInstant. В остальном методы совпадают с Temporal.PlainDateTime.
Temporal.Duration
Temporal.Duration представляет из себя какую-либо продолжительность в различных единицах времени (год, месяц, день, час, минута, секунда и т.д.).
const duration = new Temporal.Duration(0, 1, 1, 5, 2, 30); // "1 мес. 1 нед. 5 дн. 2 ч 30 мин" |
Такую продолжительность времени можно использовать при расчётах с помощью методов add и subtract. Например, добавить/вычесть дни. При этом можно использовать компоненты времени даже при расчётах дат: так 24 часа суммируются в один день для Temporal.PlainDate.
const duration = Temporal.Duration.from({ months: 1, days: 1, hours: 24 }); // 05.01.2025 |
Такие вычисления применимы не только к датам, но и ко времени
const duration = Temporal.Duration.from({ hours: 5, minutes: 30 }); |
Методы add и subtract применимы и к Temporal.Duration – можно добавлять продолжительность времени к другой продолжительности.
Объект Temporal.Duration также можно получить при сравнении двух дат. Например, через методы since или until мы можем сравнить две даты и получить продолжительность времени между ними, что довольно удобно.
// 05.01.2025 12:30:00 |
У Temporal.Duration есть несколько свойств, помимо компонентов времени, которые могут пригодиться в разработке. Свойство blank говорит о том, что длительность не является пустой и не имеет нулевых компонентов времени.
const duration = Temporal.Duration.from({ hours: 0 }); |
Метод negated позволяет полностью инвертировать продолжительность. Таким образом мы можем изменить направление длительности, при этом меняется знак sign. Это как умножение числа на -1: дважды проделанная операция не изменит знак.
const duration = Temporal.Duration.from({ days: 3, hours: 12 }); |
Используя метод total, можно быстро подсчитать общее кол-во дней, часов, минут и т.д. в продолжительности времени.
const duration = Temporal.Duration.from({ days: 1, hours: 24 }); |
С помощью метода with можно заменить какие-нибудь компоненты времени, не меняя остальные.
const duration = Temporal.Duration.from({ days: 1, hours: 24 }); |
Иммутабельность
Как можно было заметить, в отличие от Date, Temporal предоставляет иммутабельные объекты, методы которых не меняют объект, а возвращают новый. Такое поведение является более предсказуемым и безопасным.
const date = Temporal.PlainDate.from('2025-01-05'); |
Автокоррекция
Как и у Date, у Temporal тоже есть автокоррекция, но на этот раз она работает довольно ожидаемо и понятно. При создании даты/времени через объект, если установить какой-нибудь компонент времени вне пределов допустимого значения, он автоматически вернётся к максимально или минимально допустимому значению. Такое поведение может сильно упростить задачу, когда мы имеем дело с пользовательским вводом.
// 31.02.2025 (!) |
Однако, если нас такое поведение не устраивает, автокоррекцию можно настроить через параметр overflow. При значении reject, автокоррекции не будет, зато вместо неё будет выкидываться ошибка. Значение constrain ставится по умолчанию и включает автокоррекцию из предыдущего примера.
try { |
Создание некорректной даты из строки не будет включать автокоррекцию, а сразу выведет ошибку, как в поведении при overflow, равный reject.
Когда мы хотим провести какие-нибудь вычисления, то автокоррекция тут как кстати. При переполнении какого-либо компонента времени, дата будет скорректирована на новую
// 28.02.2025 |
Однако и тут есть нюансы. Например, если мы добавим 1 месяц к 31 января, то мы получим 28 февраля, а не март месяц. В целом, это логично, ведь 1 месяц – общее понятие, которое не содержит в себе чёткое количество дней. Иначе, если мы хотели бы перейти на март, то следовало бы добавлять дни, а не месяцы.
// 31.01.2025 |
Автокоррекция так же, как и для объектов, может быть настроена для методов add и subtract через параметр overflow.
Невалидная дата
В Temporal проверка невалидных дат проходит через механизм исключений. При попытке создать создать объект с невалидными данными, Temporal выбрасывает исключение. Таким образом, проверять валидность даты следует через блок try-catch.
const isValidDate = (date: Temporal.PlainDateLike | string) => { |
Но в таком случае мы проверяем только Temporal.PlainDate. Если на проекте множество объектов Temporal, то понадобится либо несколько реализаций таких функций на каждый объект, либо универсальная функция, проверяющая несколько объектов.
Сравнение дат
Temporal в своих объектах предоставляет методы для сравнения дат и времени. Например, в большинстве объектах есть метод compare, который принимает два объекта, схожих с типом в котором используется compare. Например, если мы сравниваем даты через Temporal.PlainDate.compare, то нужно, чтобы в каждом параметре были компоненты даты (год, месяц, день).
// 01.01.2025 |
Также в инициализированных объектах (которые уже имеют установленную дату и время) есть метод equals, который сравнивает даты или время. Но, в отличие от compare, который сравнивает моменты времени без учёта часового пояса, equals также сравнивает и часовой пояс. Даже в одинаковых моментах времени, разные часовые пояса, меняют возвращаемое значение в equals. Таким образом, когда compare может вернуть 0 (одинаковые даты), equals может показать false. В целом, такое поведение обосновано, ведь даже при одинаковом смещении часовые пояса могут показывать разное время, так как есть ещё понятие летнего времени DST, которое у каждого пояса своё.
// 05.01.2025 12:30 UTC+3 |
Форматирование даты
Для форматирования дат и времени полифилл Temporal предоставляет свой объект Intl.DateTimeFormat, который имеет некоторые отличия от стандартного.
const date = Temporal.PlainDate.from('2025-01-05'); |
В целом, этот объект похож на стандартный Intl.DateTimeFormat только с дополнительными опциями под Temporal, вроде календаря и т.п.
Итог
Как можно заметить, в Temporal и правда исправлены многие недостатки Date:
введены иммутабельные объекты;
добавлена поддержка часовых поясов, а также календарей и летнего времени;
понятная нумерация дней недели и месяцев;
работа с датой и временем стала более понятной, поправлена автокоррекция.
Однако форматирование, в целом, никак не изменилось по сравнению с тем же Date. Несмотря на это, можно сказать, что Temporal API – мощный инструмент для работы с датами и временем. Хоть на данный момент разработчики настоятельно не рекомендуют использовать Temporal в качестве полифилла на продовой среде, в будущем такой инструмент может сильно облегчить разработку.
SWATOPLUS
Temporal действительно классная вещь, но мы его уже лет 5 ждём. Я не понимаю почему коммитет тянет. Это очень важный стандарт и нужно бросить все силы, что бы довести его до stage 4.