Пошук уроків, статей та іншого контенту
Познайомитеся з Temporal як сучасною моделлю для миттєвостей, календарних дат, часових зон і тривалостей.
Date у JavaScript поєднує кілька різних понять в одному об’єкті:
момент на часовій шкалі;
локальне представлення моменту;
календарну дату;
час доби;
часову зону;
тривалість.
Через це код із Date часто стає неочевидним:
const date = new Date("2025-03-30T00:00:00Z");
date.setDate(date.getDate() + 1);Результат залежить від локальної часової зони середовища, у якому виконується код. Крім того, Date має змінний стан, а його парсинг рядків історично містить неоднозначності.
Temporal розділяє різні типи значень і робить їхню семантику явною:
Temporal.Instant — абсолютний момент часу;
Temporal.ZonedDateTime — момент разом із часовою зоною та календарем;
Temporal.PlainDate — календарна дата без часу й часової зони;
Temporal.PlainTime — час доби без дати та часової зони;
Temporal.PlainDateTime — дата й час без часової зони;
Temporal.Duration — тривалість;
Temporal.Calendar — календарна система;
Temporal.TimeZone — правила часової зони.
Temporal може бути доступним нативно в конкретному середовищі JavaScript. Якщо середовище його ще не підтримує, використовуйте офіційний polyfill @js-temporal/polyfill.
Для прикладів із polyfill створіть файл lesson.mjs і встановіть пакет:
npm install @js-temporal/polyfillПісля цього файл можна запустити командою:
node lesson.mjsБазовий приклад:
import { Temporal } from "@js-temporal/polyfill";
const instant = Temporal.Instant.from("2025-03-08T12:30:00Z");
console.log(instant.toString());
console.log(instant.epochMilliseconds);
const kyivTime = instant.toZonedDateTimeISO("Europe/Kyiv");
console.log(kyivTime.toString());Instant зберігає момент незалежно від локальної часової зони. Перетворення в ZonedDateTime додає правила конкретної зони.
Temporal.Instant: абсолютний моментTemporal.Instant описує точку на часовій шкалі. Він не містить:
календарної дати в конкретній часовій зоні;
локального часу;
назви часової зони.
Момент зазвичай записують у форматі ISO 8601 із суфіксом Z:
const instant = Temporal.Instant.from("2025-06-01T10:15:00Z");
console.log(instant.toString());
// 2025-06-01T10:15:00ZСуфікс Z означає UTC. Також можна створити момент із кількістю наносекунд від початку Unix epoch:
const instant = Temporal.Instant.fromEpochMilliseconds(0);
console.log(instant.toString());
// 1970-01-01T00:00:00ZДля поточного моменту використовуйте Temporal.Now:
const now = Temporal.Now.instant();
console.log(now.toString());InstantInstant підходить для:
часу створення запису в базі даних;
часу отримання HTTP-запиту;
часу завершення операції;
аудиту та журналювання;
порівняння подій на часовій шкалі.
const startedAt = Temporal.Now.instant();
// Імітація роботи операції
const finishedAt = startedAt.add({ seconds: 3 });
console.log(finishedAt.since(startedAt).toString());
// PT3SДля подій, які повинні мати однаковий абсолютний час у всіх користувачів, зберігайте Instant або ISO-рядок із часовим зміщенням.
Temporal.PlainDate: календарна датаPlainDate описує дату без часу та часової зони:
const birthday = Temporal.PlainDate.from("1990-05-17");
console.log(birthday.year);
console.log(birthday.month);
console.log(birthday.day);Така модель підходить для:
дня народження;
державного свята;
дати початку навчання;
дати рахунку;
дедлайну, якщо він визначений саме як календарний день.
Дата 2025-03-30 не означає опівночі в Києві, Лондоні чи Нью-Йорку. Це лише календарна дата.
Об’єкти Temporal незмінні. Методи add, subtract і with повертають нові значення:
const date = Temporal.PlainDate.from("2025-01-31");
const nextMonth = date.add({ months: 1 });
const previousWeek = date.subtract({ weeks: 1 });
const changedYear = date.with({ year: 2030 });
console.log(date.toString());
// 2025-01-31
console.log(nextMonth.toString());
// 2025-02-28
console.log(previousWeek.toString());
console.log(changedYear.toString());Під час додавання календарних місяців Temporal застосовує правила календаря. Оскільки 31 лютого не існує, результатом для 2025-01-31 + 1 month є 28 лютого.
Для явно заданої політики переповнення можна використати параметр overflow:
const date = Temporal.PlainDate.from("2025-01-31");
const constrained = date.add(
{ months: 1 },
{ overflow: "constrain" }
);
console.log(constrained.toString());
// 2025-02-28Значення "reject" змусить операцію завершитися помилкою, якщо результат неможливий:
const date = Temporal.PlainDate.from("2025-01-31");
try {
date.add({ months: 1 }, { overflow: "reject" });
} catch (error) {
console.log(error.constructor.name);
// RangeError
}Temporal.PlainTime: час добиPlainTime описує час без дати та часової зони:
const meetingTime = Temporal.PlainTime.from("09:30:00");
console.log(meetingTime.hour);
console.log(meetingTime.minute);Це підходить для розкладу, наприклад:
заняття починається о 09:30;
магазин працює до 18:00;
нагадування має спрацювати о 08:00 за локальним часом користувача.
const time = Temporal.PlainTime.from("09:30");
const later = time.add({ hours: 2, minutes: 45 });
console.log(later.toString());
// 12:15:00PlainTime не відповідає конкретному моменту. Щоб отримати момент, потрібні дата й часова зона.
Temporal.PlainDateTime: дата й час без зониPlainDateTime об’єднує календарну дату та час доби, але все ще не містить часової зони:
const localDateTime = Temporal.PlainDateTime.from(
"2025-10-26T09:30"
);
console.log(localDateTime.toString());Цей тип корисний, коли значення ще не прив’язане до зони:
користувач ввів дату й час у формі;
розклад зберігається як локальний час;
потрібно спочатку вибрати часову зону, а вже потім отримати момент.
Сам по собі рядок 2025-10-26T09:30 не відповідає унікальному моменту у світі.
Temporal.ZonedDateTime: момент у часовій зоніZonedDateTime містить:
дату;
час;
часову зону;
календар;
зміщення від UTC.
Його можна створити безпосередньо:
const meeting = Temporal.ZonedDateTime.from({
timeZone: "Europe/Kyiv",
year: 2025,
month: 3,
day: 8,
hour: 14,
minute: 30
});
console.log(meeting.toString());Або перетворити Instant у локальне представлення:
const instant = Temporal.Instant.from("2025-03-08T12:30:00Z");
const kyiv = instant.toZonedDateTimeISO("Europe/Kyiv");
const newYork = instant.toZonedDateTimeISO("America/New_York");
console.log(kyiv.toString());
console.log(newYork.toString());Обидва значення описують той самий момент, але показують його в різних часових зонах.
console.log(kyiv.toInstant().equals(newYork.toInstant()));
// trueЯкщо є PlainDateTime і назва часової зони:
const localDateTime = Temporal.PlainDateTime.from(
"2025-03-08T14:30"
);
const zonedDateTime = localDateTime.toZonedDateTime(
"Europe/Kyiv"
);
const instant = zonedDateTime.toInstant();
console.log(zonedDateTime.toString());
console.log(instant.toString());Це перетворення може бути неоднозначним під час переходу на літній або зимовий час.
У деяких часових зонах певний локальний час:
не існує;
зустрічається двічі.
Наприклад, під час переходу на літній час годинник може перестрибнути з 02:59 на 04:00. Локальний час 03:30 у такій зоні не існує.
Під час переходу назад одна година повторюється. Наприклад, 02:30 може відповідати двом різним моментам.
Temporal дозволяє вказати політику для неоднозначних значень:
"compatible" — стандартна сумісна поведінка;
"earlier" — вибрати ранній варіант;
"later" — вибрати пізній варіант;
"reject" — викинути помилку.
const localDateTime = Temporal.PlainDateTime.from(
"2025-10-26T02:30"
);
const earlier = localDateTime.toZonedDateTime(
"Europe/Kyiv",
{ disambiguation: "earlier" }
);
const later = localDateTime.toZonedDateTime(
"Europe/Kyiv",
{ disambiguation: "later" }
);
console.log(earlier.toInstant().toString());
console.log(later.toInstant().toString());Між earlier і later може бути різниця в одну годину, хоча локальні дата й час однакові.
Для критичних систем краще не покладатися на значення за замовчуванням. Вибір політики варто зробити частиною бізнес-правил.
Temporal розрізняє календарну арифметику та арифметику тривалостями.
Розглянемо день переходу на літній час:
import { Temporal } from "@js-temporal/polyfill";
const start = Temporal.ZonedDateTime.from({
timeZone: "Europe/Kyiv",
year: 2025,
month: 3,
day: 29,
hour: 12
});
const nextCalendarDay = start.add({ days: 1 });
const next24Hours = start.add({ hours: 24 });
console.log(start.toString());
console.log(nextCalendarDay.toString());
console.log(next24Hours.toString());
console.log(
nextCalendarDay.since(start).toString()
);
console.log(
next24Hours.since(start).toString()
);add({ days: 1 }) означає «такий самий локальний час наступного календарного дня». Якщо через перехід на літній час доба тривала 23 години, абсолютна різниця може становити 23 години.
add({ hours: 24 }) означає додавання саме 24 годин на часовій шкалі. Локальний час після операції може відрізнятися.
Це важлива відмінність:
для зустрічі щодня о 09:00 використовуйте календарні дні;
для таймера на 24 години використовуйте години або секунди;
для терміну дії токена використовуйте абсолютний момент і фіксовану тривалість.
Temporal.Duration: тривалостіDuration описує кількість одиниць часу:
const duration = Temporal.Duration.from({
days: 2,
hours: 4,
minutes: 30
});
console.log(duration.toString());
// P2DT4H30MТривалість можна додавати та віднімати:
const base = Temporal.PlainDateTime.from(
"2025-05-10T08:00"
);
const result = base.add({
days: 1,
hours: 2,
minutes: 15
});
console.log(result.toString());
// 2025-05-11T10:15:00Місяць і рік не мають фіксованої кількості годин. Навіть день у часовій зоні може мати 23, 24 або 25 годин.
Тому не завжди можна без контексту перетворити тривалість на години:
const duration = Temporal.Duration.from({ months: 1 });
try {
console.log(duration.total({ unit: "hours" }));
} catch (error) {
console.log(error.constructor.name);
// RangeError
}Для календарних одиниць потрібно вказати relativeTo:
const duration = Temporal.Duration.from({ months: 1 });
const totalDays = duration.total({
unit: "days",
relativeTo: Temporal.PlainDate.from("2025-01-01")
});
console.log(totalDays);
// 31Результат залежить від початкової дати. Один місяць від 1 лютого має іншу кількість днів, ніж один місяць від 1 січня.
Для Temporal існують статичні методи compare, а також методи equals.
const first = Temporal.PlainDate.from("2025-01-01");
const second = Temporal.PlainDate.from("2025-02-01");
console.log(Temporal.PlainDate.compare(first, second));
// -1
console.log(first.equals(second));
// falseДля моментів:
const first = Temporal.Instant.from("2025-01-01T00:00:00Z");
const second = Temporal.Instant.from("2025-01-02T00:00:00Z");
console.log(Temporal.Instant.compare(first, second));
// -1equals перевіряє повну рівність значень конкретного типу. Два ZonedDateTime можуть представляти один момент, але мати різні часові зони:
const instant = Temporal.Instant.from("2025-01-01T12:00:00Z");
const kyiv = instant.toZonedDateTimeISO("Europe/Kyiv");
const london = instant.toZonedDateTimeISO("Europe/London");
console.log(kyiv.toInstant().equals(london.toInstant()));
// true
console.log(kyiv.equals(london));
// falseДля бізнес-логіки потрібно вирішити, що саме порівнюється:
абсолютний момент;
локальна дата;
дата, час і зона;
повне структуроване значення.
Усі основні типи Temporal мають toString():
const date = Temporal.PlainDate.from("2025-05-17");
const instant = Temporal.Instant.from("2025-05-17T12:00:00Z");
console.log(date.toString());
// 2025-05-17
console.log(instant.toString());
// 2025-05-17T12:00:00ZДля JSON зручно серіалізувати значення як рядки:
const payload = {
createdAt: Temporal.Now.instant().toString(),
dueDate: Temporal.PlainDate.from("2025-12-31").toString()
};
const json = JSON.stringify(payload);
console.log(json);Після отримання даних із JSON потрібно явно відновити тип:
const parsed = JSON.parse(json);
const createdAt = Temporal.Instant.from(parsed.createdAt);
const dueDate = Temporal.PlainDate.from(parsed.dueDate);
console.log(createdAt.toString());
console.log(dueDate.toString());Не варто передавати календарну дату як Instant, якщо вона не має конкретного моменту. Наприклад, дата народження користувача не повинна автоматично перетворюватися на опівніч у часовій зоні сервера.
const createdAt = Temporal.Now.instant();Зберігайте як абсолютний момент.
const birthDate = Temporal.PlainDate.from("1988-11-04");Часова зона для такого значення не потрібна.
Окремо зберігайте:
const schedule = {
date: Temporal.PlainDate.from("2025-09-01"),
time: Temporal.PlainTime.from("09:00"),
timeZone: "Europe/Kyiv"
};Якщо потрібно отримати конкретний момент:
const localDateTime = schedule.date.toPlainDateTime(schedule.time);
const occurrence = localDateTime.toZonedDateTime(
schedule.timeZone
);
console.log(occurrence.toString());const expiresAt = Temporal.Now.instant().add({
minutes: 30
});Для перевірки:
const now = Temporal.Now.instant();
if (Temporal.Instant.compare(now, expiresAt) >= 0) {
console.log("Сесія завершилася");
}Нижче наведено повний приклад планування події та її відображення для користувача:
import { Temporal } from "@js-temporal/polyfill";
const event = {
title: "Розбір архітектури",
date: Temporal.PlainDate.from("2025-10-26"),
time: Temporal.PlainTime.from("09:30"),
timeZone: "Europe/Kyiv"
};
const localDateTime = event.date.toPlainDateTime(event.time);
const scheduled = localDateTime.toZonedDateTime(
event.timeZone,
{ disambiguation: "reject" }
);
const instant = scheduled.toInstant();
console.log(`${event.title}:`);
console.log(`Локальний час: ${scheduled.toString()}`);
console.log(`Момент UTC: ${instant.toString()}`);
const forNewYork = instant.toZonedDateTimeISO(
"America/New_York"
);
console.log(
`Для Нью-Йорка: ${forNewYork.toString()}`
);
const serialized = {
title: event.title,
instant: instant.toString(),
timeZone: event.timeZone
};
console.log(JSON.stringify(serialized));У цьому прикладі:
дата й час події представлені як локальні значення;
часова зона вибирається явно;
неоднозначний локальний час відхиляється;
для передачі між системами зберігається абсолютний момент;
для відображення користувачу момент перетворюється у його часову зону.
DateDate не потрібно негайно вилучати з усіх проєктів. Він залишається поширеним у старих API та бібліотеках.
Конвертація Date у Temporal.Instant:
import { Temporal } from "@js-temporal/polyfill";
const legacyDate = new Date("2025-05-17T12:00:00Z");
const instant = Temporal.Instant.fromEpochMilliseconds(
legacyDate.getTime()
);
console.log(instant.toString());Конвертація Temporal.Instant у Date:
const instant = Temporal.Instant.from("2025-05-17T12:00:00Z");
const legacyDate = new Date(
Number(instant.epochMilliseconds)
);
console.log(legacyDate.toISOString());Date зберігає мілісекундну точність, тоді як Instant підтримує вищу точність. Під час конвертації в Date точність може бути втрачена.
Не використовуйте new Date("2025-05-17") як заміну PlainDate, якщо вам потрібна саме календарна дата. Date усе одно інтерпретує значення через модель моменту часу.
Temporal за замовчуванням використовує григоріанський календар, але модель підтримує календарні системи.
У більшості прикладних задач достатньо ISO-календаря:
const date = Temporal.PlainDate.from("2025-05-17");
console.log(date.calendarId);
// iso8601Якщо застосунок працює з іншою календарною системою, не можна бездумно трактувати всі поля як григоріанські. Операції з роками, місяцями та днями повинні виконуватися з урахуванням календаря.
У фінансових, державних і міжнародних системах календар — це частина доменної моделі, а не лише формат відображення.
Об’єкти Temporal незмінні:
const original = Temporal.PlainDate.from("2025-01-01");
const changed = original.add({ days: 1 });
console.log(original.toString());
// 2025-01-01
console.log(changed.toString());
// 2025-01-02Це спрощує:
функціональний стиль програмування;
кешування;
повторне використання значень;
аналіз стану в UI;
тестування.
Метод with також не змінює початкове значення:
const date = Temporal.PlainDate.from("2025-06-10");
const moved = date.with({ day: 20 });
console.log(date.toString());
console.log(moved.toString());Не викликайте Temporal.Now.instant() усередині кожної функції, яку потрібно тестувати. Передавайте поточний момент як залежність:
import { Temporal } from "@js-temporal/polyfill";
function isExpired(expiresAt, now) {
return Temporal.Instant.compare(now, expiresAt) >= 0;
}
const now = Temporal.Instant.from("2025-01-01T12:00:00Z");
const expiresAt = Temporal.Instant.from("2025-01-01T12:30:00Z");
console.log(isExpired(expiresAt, now));
// false
console.log(
isExpired(
expiresAt,
Temporal.Instant.from("2025-01-01T12:30:00Z")
)
);
// trueТакі функції мають передбачуваний результат і не залежать від реального системного годинника.
Instant для календарної датиНеправильно:
const birthday = Temporal.Instant.from(
"1990-05-17T00:00:00Z"
);Це конкретний момент, який у різних часових зонах може показувати різні календарні дати.
Правильно:
const birthday = Temporal.PlainDate.from("1990-05-17");PlainDateTime як абсолютного часуPlainDateTime не має часової зони, тому його недостатньо для надсилання події в міжнародну систему. Спочатку потрібно додати часову зону, а потім отримати Instant.
add({ hours: 24 }) і add({ days: 1 }) можуть мати різний результат у часовій зоні з переходами на літній час.
Вибирайте одиницю відповідно до змісту операції:
days — календарна логіка;
hours, minutes, seconds — фіксована тривалість.
Рядок 2025-08-20T09:00 не повідомляє, де саме відбувається подія. Для міжнародних систем зберігайте назву зони на кшталт Europe/Kyiv, а не лише числове зміщення +03:00.
Числове зміщення може змінюватися залежно від дати. Назва часової зони посилається на набір правил.
Не змішуйте в одному полі:
рядок календарної дати;
Date;
Instant;
ZonedDateTime.
На межах системи використовуйте явно визначений формат, а після отримання даних одразу перетворюйте їх на відповідний Temporal-тип.
Під час переходів між стандартним і літнім часом локальний час може бути відсутнім або повторюватися. Для платіжних операцій, бронювань і розкладів краще явно задати disambiguation.
Temporal.Instant описує абсолютний момент часу.
Temporal.PlainDate описує календарну дату без часової зони.
Temporal.PlainTime описує час доби.
Temporal.PlainDateTime описує дату й час без часової зони.
Temporal.ZonedDateTime поєднує момент, календарну дату, час і часову зону.
Temporal.Duration описує тривалість, але календарні одиниці потребують контексту.
Temporal-об’єкти незмінні.
Арифметика календарними днями може відрізнятися від додавання фіксованої кількості годин.
Для міжнародних систем слід явно зберігати часову зону та розділяти локальні значення від абсолютних моментів.
Для взаємодії зі старим кодом використовуйте явне перетворення між Date і Temporal.Instant.
Якщо середовище не підтримує Temporal нативно, застосовуйте @js-temporal/polyfill.