Temporal API в JavaScript
Temporal API це новий стандартний набір типів для роботи з датами й часом у JavaScript, який має замінити Date. Це пропозиція TC39, що дійшла до Stage 3, вже реалізована в частині рушіїв і доступна скрізь через офіційний polyfill. Головна ідея: незмінні об'єкти, чітке розділення понять «момент», «дата», «час» і «часовий пояс», плюс точна робота з базою часових поясів IANA.
Теорія
TL;DR
Temporalце нове вбудоване API дат і часу; воно не розширюєDate, а повністю його замінює.- Усі об'єкти незмінні:
add(),subtract(),with(),round()повертають нове значення. - Окремі типи під окремі задачі:
Instant,PlainDate,PlainTime,PlainDateTime,ZonedDateTime,Duration,Now. - Часовий пояс зберігається як частина значення (
[Europe/Kyiv]), а не лише як зміщення+03:00, тому переходи на літній та зимовий час обробляються правильно. - Некоректна дата дає
RangeError(зoverflow: 'reject') або явний clamp замість тихого «переносу», як уnew Date(2025, 1, 30). - Місяці нумеруються з 1, а не з 0, як у
Date. - Форматування виводу віддається
Intl.DateTimeFormat; у старих середовищах підключають@js-temporal/polyfill.
Швидкий приклад
// Дата без часу
const date = Temporal.PlainDate.from('2025-10-16');
console.log(date.year, date.month, date.day); // 2025 10 16
// Час без дати
const time = Temporal.PlainTime.from('13:45:30');
console.log(time.hour); // 13
// Дата й час без часового поясу
const dt = Temporal.PlainDateTime.from('2025-10-16T13:45:30');
console.log(dt.toString()); // 2025-10-16T13:45:30Чому Date не вистачає
Date це одна з найстаріших і найпроблемніших частин мови: його API скопіювали з Java ще у 1995 році й фактично не змінювали.
| Проблема | Приклад |
|---|---|
| Неявні часові пояси | new Date('2024-01-01') парситься як UTC, а new Date('2024-01-01T00:00') як локальний час |
| Непередбачувані обчислення | new Date(2024, 1, 30) дає 1 березня, бо лютого 30 не існує |
| Мутуючі методи | setMonth(), setHours() змінюють вихідний об'єкт, а не повертають новий |
| Незручна робота з UTC | доводиться вручну жонглювати toISOString() і парою getHours() / getUTCHours() |
| Нумерація з нуля | місяці 0..11, а дні місяця 1..31 |
| Немає типів для календарів, тривалостей і зон | за будь-якою дрібницею тягнули сторонню бібліотеку: Moment, date-fns, Luxon |
Саме тому Temporal не «лагодить» Date, а вводить окрему, послідовну модель часу.
Класи Temporal
| Клас | Що представляє | Приклад |
|---|---|---|
Temporal.Instant | момент на абсолютній шкалі часу (точка в UTC) | "2025-10-16T21:00:00Z" |
Temporal.PlainDate | дата без часу | "2025-10-16" |
Temporal.PlainTime | час без дати | "21:00:00" |
Temporal.PlainDateTime | дата й час без часового поясу | "2025-10-16T21:00:00" |
Temporal.ZonedDateTime | дата + час + часовий пояс | "2025-10-16T21:00:00+03:00[Europe/Kyiv]" |
Temporal.Duration | тривалість, різниця між моментами | "P3DT5H" (3 дні 5 годин) |
Temporal.Now | утиліта доступу до поточного часу | Temporal.Now.instant() |
Різниця між PlainDateTime і ZonedDateTime ключова на співбесіді. PlainDateTime це «настінний» час без прив'язки до місця: «16 жовтня о 21:00» може означати різні миті в різних країнах. ZonedDateTime містить зону, тому однозначно вказує на конкретну мить і знає про переходи DST. Коли вам потрібна саме мить (мітка події, запис у лог), беріть Instant або ZonedDateTime; коли потрібен «день народження» чи «час відкриття офісу», беріть PlainDate / PlainTime.
Часові пояси та перехід на літній час
const zdt = Temporal.ZonedDateTime.from({
timeZone: 'Europe/Kyiv',
year: 2025,
month: 10,
day: 16,
hour: 21,
});
console.log(zdt.toString());
// 2025-10-16T21:00:00+03:00[Europe/Kyiv]
console.log(zdt.toInstant().toString());
// 2025-10-16T18:00:00Z (те саме в UTC)Temporal зберігає ідентифікатор зони ([Europe/Kyiv]) як частину даних, а не лише зміщення +03:00. Завдяки цьому арифметика враховує переходи на літній та зимовий час:
const beforeDST = Temporal.ZonedDateTime.from('2025-03-30T01:30:00+01:00[Europe/Berlin]');
const afterDST = beforeDST.add({ hours: 1 });
console.log(afterDST.toString());
// 2025-03-30T03:30:00+02:00[Europe/Berlin]Годинник «перестрибнув» з 01:30 на 03:30, бо о 02:00 стрілки перевели вперед: додали одну реальну годину, і зміщення змінилося з +01:00 на +02:00.
Поточний час беруть через Temporal.Now:
const now = Temporal.Now.instant();
console.log(now.toString()); // поточний час у UTC
const kyiv = Temporal.Now.zonedDateTimeISO('Europe/Kyiv');
console.log(kyiv.toString());
// наприклад: 2025-10-16T23:50:00+03:00[Europe/Kyiv]Обчислення, Duration і сувора валідація
Усі операції повертають нові значення, вихідний об'єкт залишається недоторканим:
const today = Temporal.PlainDate.from('2025-10-16');
const tomorrow = today.add({ days: 1 });
const lastWeek = today.subtract({ weeks: 1 });
console.log(tomorrow.toString()); // 2025-10-17
console.log(lastWeek.toString()); // 2025-10-09
console.log(today.toString()); // 2025-10-16, не змінивсяРізниця між двома моментами це Temporal.Duration в ISO 8601 форматі:
const start = Temporal.PlainDateTime.from('2025-10-16T10:00');
const end = Temporal.PlainDateTime.from('2025-10-18T15:30');
const duration = end.since(start);
console.log(duration.toString()); // P2DT5H30M (2 дні 5 годин 30 хвилин)
console.log(duration.days, duration.hours, duration.minutes); // 2 5 30Некоректні значення не «виправляються» тихцем. Рядок з неіснуючою датою одразу кидає помилку, а для об'єкта поведінку задає опція overflow:
// Старий Date мовчки переносить дату
new Date(2025, 1, 30); // 2025-03-02 (місяць 1 це лютий, 30 лютого не існує)
// Temporal: рядок з неіснуючою датою одразу помилка
Temporal.PlainDate.from('2025-02-30'); // RangeError
// Для об'єкта за замовчуванням overflow: 'constrain' (обрізає до межі місяця)
Temporal.PlainDate.from({ year: 2025, month: 2, day: 30 }); // 2025-02-28
// Суворий режим
Temporal.PlainDate.from({ year: 2025, month: 2, day: 30 }, { overflow: 'reject' }); // RangeErrorЗверніть увагу: у Temporal month: 2 це лютий, бо місяці нумеруються з 1. У Date та сама позиція означала б березень.
Переваги, форматування та підтримка
| Перевага | Що дає |
|---|---|
| Незмінність | безпечні операції без мутацій і без випадкових побічних ефектів |
| Чітка модель | розділення понять «момент», «дата», «час», «зона» |
| Точні часові пояси | база IANA, коректний DST, зона всередині значення |
Немає «магії» Date | помилки замість неявних коригувань |
Сумісність з Intl | форматування для користувача без ручного склеювання рядків |
ISO 8601 і Duration | проста робота з різницями та інтервалами |
Форматування віддано Intl.DateTimeFormat, тому локалізація виглядає звично:
const zdt = Temporal.ZonedDateTime.from('2025-10-16T21:00:00+03:00[Europe/Kyiv]');
const fmt = new Intl.DateTimeFormat('uk-UA', { dateStyle: 'full', timeStyle: 'long' });
console.log(fmt.format(zdt));
// четвер, 16 жовтня 2025 р. о 21:00:00 за східноєвропейським літнім часомДе вже можна користуватися:
- Найновіші рушії постачають
Temporalнативно (першим повну реалізацію включив Firefox), у решти вона в роботі. - У Node.js і старих браузерах підключають офіційний polyfill.
- Перед використанням у проді перевіряйте підтримку на MDN або через
typeof Temporal !== 'undefined'.
npm i @js-temporal/polyfillimport { Temporal } from '@js-temporal/polyfill';
const date = Temporal.PlainDate.from('2025-10-16');
console.log(date.toString()); // 2025-10-16Типові помилки
- Очікувати мутації.
dt.add({ days: 1 })нічого не змінює вdt; результат треба присвоїти. Це найчастіша помилка для тих, хто звик доsetDate(). - Плутати
PlainDateTimeіZonedDateTime.PlainDateTimeне є миттю на шкалі часу: без зони його не можна коректно порівняти зInstantчи зберегти як мітку події. - Зберігати лише зміщення.
+03:00це не часовий пояс: наступного тижня після переходу на зимовий час те саме місце вже матиме+02:00. Зберігайте ідентифікатор IANA (Europe/Kyiv). - Нумерація місяців з нуля. Перенісши код з
Date, легко залишитиmonth: 0; у Temporal цеRangeError, бо січень це1. - Розраховувати на
RangeErrorтам, де дієconstrain.from({...})за замовчуванням обрізає значення до межі місяця; щоб отримати помилку, передайте{ overflow: 'reject' }. - Змішувати
DateіTemporalу розрахунках. Конвертуйте явно:date.toTemporalInstant()таinstant.epochMilliseconds. - Тягнути важку бібліотеку задля
Temporal. Сам polyfill теж важить чимало, тож для одного форматування іноді достатньоIntl.DateTimeFormatнад звичайнимDate.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.