Коротка відповідь. Node.js 26 увімкнув Temporal за замовчуванням – без флагів. Головна зміна ментальна, а не синтаксична: Temporal розділяє типами те, що Date тридцять років змішував в одному об'єкті. Instant – момент часу (нічого не знає про календарі), PlainDate – дата без часу й зони («день народження»), ZonedDateTime – час на стіні в конкретному місці з усіма наслідками DST. Обираєш неправильний тип – помилку видно в коді, а не в проді о 03:00 у неділю переведення годинників. Нижче – коли який тип, кейс Валенсія–Лондон через жовтневий DST і план міграції без big bang.

Чому Date небезпечний у бізнес-логіці

Date – це один тип на три різні поняття, і кожне змішування – майбутній баг:

// Що це: момент часу? день? локальний час?
const d = new Date('2026-10-25');
// У Node це опівніч UTC. У локальній зоні Валенсії – ще 24.10, 23:00... ні,
// стоп, у жовтні +2... вже заплутався? Саме так виглядає half-багів із датами.

Додай сюди мутабельність (setMonth змінює об'єкт на місці), місяці з нуля, мовчазні переповнення (new Date(2026, 1, 30) – це 2 березня) – і стає зрозуміло, чому «робота з датами» – стабільний топ продакшн-інцидентів.

Temporal, який у Node.js 26 увімкнено за замовчуванням (Node 26 стає LTS у жовтні 2026), вирішує це не «зручнішими методами», а системою типів.

Instant vs PlainDate vs ZonedDateTime

Три типи – три різні запитання:

Тип Відповідає на Приклади
Temporal.Instant «Коли це сталось у фізичному часі?» created_at у базі, лог, підпис токена
Temporal.PlainDate «Який це день у календарі?» день народження, дедлайн «до 15-го», дата рахунку
Temporal.ZonedDateTime «Котра година на стіні в цьому місці?» зустріч о 10:00 у Валенсії, відкриття магазину
// Момент: нічого не знає про зони й календарі
const paidAt = Temporal.Now.instant();

// Календарна дата: нічого не знає про години і зони
const invoiceDate = Temporal.PlainDate.from('2026-10-01');

// Час на стіні + зона: знає про DST усе
const meeting = Temporal.ZonedDateTime.from(
  '2026-10-22T10:00:00[Europe/Madrid]'
);

Є й допоміжні: PlainTime («щодня о 08:30»), PlainDateTime (дата+час, зона ще невідома), Duration (інтервали), PlainYearMonth (білінговий період). Усі – імутабельні: будь-яка операція повертає новий об'єкт.

Правило вибору просте: якщо ти зберігаєш «коли сталося» – Instant; якщо «на який день домовились» – PlainDate; якщо «котра година у конкретному місті» – ZonedDateTime. Більшість болючих багів – це Instant там, де мав бути ZonedDateTime, і навпаки.

Кейс: зустріч Валенсія–Лондон через перехід на зимовий час

Менторська класика: щотижнева зустріч «четвер, 10:00 за Валенсією» з учасником у Лондоні. У ніч на 25 жовтня 2026 Європа переводить годинники (CEST +02:00 → CET +01:00).

Типовий баг старого світу: зустріч зберегли як UTC-момент («10:00 у Валенсії = 08:00 UTC») і додають по сім днів. Після переходу 08:00 UTC – це вже 09:00 за Валенсією: зустріч «з'їхала» на годину для всіх, хто живе за стінним часом.

Temporal робить правильну модель природною: повторювана подія – це стінний час + зона, а не момент:

// Правило зустрічі: четвер, 10:00, час Валенсії
const beforeShift = Temporal.ZonedDateTime.from(
  '2026-10-22T10:00:00[Europe/Madrid]'
);

// Наступний тиждень – ЧЕРЕЗ перехід на зимовий час
const afterShift = beforeShift.add({ weeks: 1 });

console.log(afterShift.toString());
// 2026-10-29T10:00:00+01:00[Europe/Madrid]
// 10:00 на стіні збережено, offset коректно змінився +02:00 → +01:00

// Погляд лондонського учасника
console.log(beforeShift.withTimeZone('Europe/London').toString());
// 2026-10-22T09:00:00+01:00[Europe/London]  (BST)
console.log(afterShift.withTimeZone('Europe/London').toString());
// 2026-10-29T09:00:00+00:00[Europe/London]  (GMT)

Арифметика пройшла через DST-межу і зробила саме те, що мали на увазі люди: «десята ранку в четвер» лишилась десятою. А конвертація withTimeZone чесно показує той самий фізичний момент очима Лондона. Той самий epochNanoseconds, різні стіни – і це видно в типах, а не в коментарях.

Бонус, який Date не вмів ніколи: Temporal знає про неіснуючі та подвійні години. 25 жовтня 02:30 у Мадриді трапляється двічі – ZonedDateTime.from(..., { disambiguation: 'earlier' | 'later' }) змушує тебе вирішити це явно, а не отримати «якусь» із двох.

Parsing, форматування, сумісність

  • Парсинг – строгий: Temporal.PlainDate.from('2026-13-45') кидає помилку, а не «переносить» місяці, як Date.
  • Форматування для людей – той самий Intl: meeting.toLocaleString('uk-UA', {...}).
  • Міст до старого світу: instant.epochMilliseconds ↔ Temporal.Instant.fromEpochMilliseconds(date.getTime()). Повного «Date.toTemporal()» не чекай – міст свідомо вузький.

Міграція без big bang

Переписувати весь проєкт – не треба. Робочий порядок:

  1. Нова логіка з датною арифметикою/зонами – одразу на Temporal.
  2. Межі системи (JSON API, база) лишаються ISO-рядками – Temporal чудово їх parse-ить і serialize-ить (toString() дає ISO з назвою зони у квадратних дужках – для зовнішніх API зазвичай шлють instant + окремо зону).
  3. Старі болючі місця (усе, де в коді є setHours, ручні * 60 * 1000 чи «+1 день» через мілісекунди) – мігруй при першому ж дотику: це найкращий ROI.
  4. У тестах фіксуй DST-кейси явно: 25.10.2026 і 29.03.2026 – готові дати для фікстур.

Чесно про підтримку поза Node 26: за MDN, у браузерах Temporal досі «limited availability» – у стабільних релізах основних браузерів його ще немає, для фронтенду потрібен polyfill (@js-temporal/polyfill або temporal-polyfill). Старі Node LTS (20/22/24) – теж тільки з polyfill або за флагом у пізніх 24.x. Тож «бекенд на Node 26 – нативно, shared-код із фронтом – поки через polyfill» – нормальна стратегія на найближчий рік.

Типові помилки

  • Instant для подій «за стіною». Зустрічі, розклади, «щодня о 9» – це ZonedDateTime/PlainTime; Instant тут і є той баг зі зсувом на годину.
  • ZonedDateTime для логів. Навпаки теж боляче: лог – це момент; тягати зону немає сенсу і дорожче в зберіганні.
  • Своя арифметика поверх epochMilliseconds. + 7 * 24 * 60 * 60 * 1000 – саме той код, який Temporal прийшов поховати.
  • Ігнор disambiguation. Опції 'earlier'/'later'/'compatible' існують, бо подвійна година реальна; дефолт 'compatible' – ок, але для білінгу обирай свідомо.
  • Очікування Temporal у браузері без polyfill. Перевір таргети перед тим, як шарити датну логіку між Node і фронтом.

Практичне завдання

Візьми з свого проєкту одну функцію, що робить датну арифметику через Date (пошукай setDate, getTime() +, * 86400). Перепиши її на Temporal, обравши правильний тип за таблицею вище. Потім напиши один тест на 25.10.2026 через межу переведення годинників – і подивись, чи стара реалізація його проходила. Для бекендерів це ще й чудова тема на співбесіді: питання «Instant чи ZonedDateTime для X?» миттєво показує, хто працював із таймзонами, а хто лише чув про них. А якщо твій API приймає дати від клієнтів із retry – перевір заодно ідемпотентність: дубль запиту «створити зустріч» через DST-годину – особливо підступний.