Temporal — nowy standard dat i czasu w JavaScript
Po wielu latach monopolu Date, 16 czerwca 2020 wyłonił się proposal nowego standardu Temporal, który ma zrewolucjonizować pracę z datami w JavaScript. Po prawie 5 latach pracy nad specyfikacją i implementacją, 27 maja 2025 został dodany do Firefox 139, a prawdopodobnie już w styczniu 2026 pojawi się wraz z wydaniem Chrome 144.
Temporal buduje solidne fundamenty pod dalszy rozwój tej specyfikacji. Prawie w ogóle nie przypomina Date, przez co całkowicie zmienia sposób, w jaki pracujemy z datami i czasem. Klasy dedykowane ich różnym reprezentacjom oraz dziesiątki wbudowanych metod i właściwości dają nowe, ogromne możliwości pracy.
Problemy Date
Date jaki jest, każdy widzi. Jest skrajnie nieintuicyjny i niespójny, a jego zachowanie bywa nieprzewidywalne. Do tego ma mylące nazewnictwo i brakuje mu podstawowych funkcjonalności.
Przyczyna jest prosta – język powstał w ciągu kilkunastu dni w 1995. W takim tempie nie da się dobrze przemyśleć pewnych konstrukcji, więc twórca języka posiłkował się gotowymi rozwiązaniami.
Date bazuje na implementacji klasy java.util.Date z języka Java. Ta klasa również pozostawiała wiele do życzenia, ponieważ już 2 lata później, w 1997, została uznana jako deprecated i zastąpiona przez java.util.Calendar.
Pierwsze z brzegu problemy, które przychodzą mi do głowy:
- Miesiące indeksowane są od 0 do 11, ale dni już od 1 do 31.
- Mamy gettery,
getMonth(),getHours,getMinutes(), ale dzień to jużgetDate(). - Wynik parsowania
.toString()zwróci datę lokalną, natomiast.toISOString()w UTC. - Konstruktor przyjmuje całą masę różnych formatów wejściowych:
0,"0","2025","2025-10-15","October 15, 2025 14:30", instancjęDate, timestamp i wiele innych. - Brak wsparcia do stref czasowych.
- Brak wyraźnego rozróżnienia między czasem lokalnym, a UTC.
Po całą resztę odsyłam do quizu jsdate.wtf. Świetna zabawa i wielkie WTF przy każdym pytaniu. ;)
Temporal 101
Od tego momentu zobaczysz mnóstwo przykładów kodu. Zachęcam cię, żebyś kopiował je do konsoli przeglądarki (Firefox 139+), podglądając wyniki i eksperymentując nimi, co dodatkowo wzmocni naukę.
Wszystko, co zobaczysz, to esencja dokumentacji MDN, a konkretnie sekcji Shared class interface. Nie poruszam wszystkich tematów zawartych w dokumentacji, bo moim celem nie jest jej skopiowanie i parafrazowanie każdego zdania. Chcę przekazać ci najważniejsze informacje, które wykorzystasz w 80% przypadków swojej pracy (#pareto).
Parsowanie
Do parsowania stringów i serializacji Temporal obsługuje standard RFC 9557. Format jest rozwinięciem ISO 8601. To znaczy, że nadal będziesz mógł pracować z tymi samymi formatami, jak dotychczas. Jedyną różnicą jest dodatkowe wsparcie dla kalendarzy, stref czasowych oraz precyzji do nanosekund.
// YYYY-MM-DD T HH:mm:ss.sssssssss Z/±HH:mm [time_zone_id] [u-ca=calendar_id]
"2025-11-03T14:35:52.12+01:00[Europe/Warsaw][u-ca=gregory]"
// YYYY-MM-DD
"2025-11-03"
// HH:mm:ss.sssssssss
"14:35:52.123456789"
// [...]
Precyzja
Temporal jest precyzyjny co do nanosekundy.
const time = Temporal.PlainTime
.from('14:35:52.123456789')
time.millisecond // => 123
time.microsecond // => 456
time.nanosecond // => 789
Klasy i typy
Temporal to namespace zawierający klasy reprezentujące różne formy daty i czasu. W przeciwieństwie do Date, który jest “wszystkim naraz”, Temporal dzieli odpowiedzialności.
Typy z prefiksem Plain są reprezentacją bez strefy czasowej:
Temporal.PlainDate– tylko data (np.2025-11-03)Temporal.PlainTime– tylko czas (np.14:30:00)Temporal.PlainDateTime– data + czas (np.2025-11-03T14:30:00)Temporal.PlainYearMonth– rok i miesiąc (np.2025-11)Temporal.PlainMonthDay– miesiąc i dzień (np.11-03)
Typy Instant oraz ZonedDateTime są reprezentacją ze strefą czasową:
Temporal.Instant– timestamp UTC (np.2025-11-03T13:30:00Z)Temporal.ZonedDateTime– data + czas + strefa (np.2025-11-03T14:30:00+01:00[Europe/Warsaw])
Dodatkowe typy:
Temporal.Duration– różnica czasowa (np.5 godzin i 30 minut)Temporal.Now– obiekt z metodami do pobierania bieżącej daty/czasu (domyślnie zwracają czas lokalny)
Zobaczmy to w praktyce:
// Data
Temporal.Now.plainDateISO()
Temporal.PlainDate.from('2025-11-03')
// => 2025-11-03
// Czas
Temporal.Now.plainTimeISO()
Temporal.PlainTime.from('14:58:41.642')
// => 14:58:41.642
// Data i czas
Temporal.Now.plainDateTimeISO()
Temporal.PlainDateTime.from('2025-11-03T14:58:25.269')
// => 2025-11-03T14:58:25.269
// Rok i miesiąc
Temporal.PlainYearMonth.from('2025-11')
Temporal.PlainYearMonth.from({ year: 2025, month: 11 })
// => 2025-11
// Miesiąc i dzień
Temporal.PlainMonthDay.from('11-03')
Temporal.PlainMonthDay.from({ month: 11, day: 3 })
// => 11-03
// Timestamp UTC
Temporal.Now.instant()
Temporal.Instant.from('2025-11-03T13:59:36.813Z')
Temporal.Instant.fromEpochMilliseconds(1730642376813)
// => 2025-11-03T13:59:36.813Z
// Data i czas ze strefą czasową
Temporal.Now.zonedDateTimeISO()
Temporal.ZonedDateTime.from('2025-11-03T14:58:14.83+01:00[Europe/Warsaw]')
// => 2025-11-03T14:58:14.83+01:00[Europe/Warsaw]
Modyfikacja wartości
Wszystkie obiekty Temporal są niemutowalne. Każda operacja zwraca nową instancję, pozostawiając oryginał bez zmian.
const date = Temporal.PlainDate.from('2025-01-01')
date.month // => 1
const modifiedDate = date.with({ month: 12 })
date.month // => 1 (oryginał niezmieniony)
modifiedDate.month // => 12 (nowa instancja)
Metoda .with() służy do nadpisywania konkretnych jednostek czasu. W zależności od typu, dostępne są różne parametry – dla PlainDate nie ustawisz godziny, dla PlainTime nie zmienisz dnia.
Temporal.PlainDate
.from('2025-10-15')
.with({ day: 20 })
// => 2025-10-20
Temporal.PlainDateTime
.from('2025-10-15T14:30')
.with({ hour: 9, minute: 0 })
// => 2025-10-15T09:00:00
Dla ZonedDateTime możesz zmienić strefę czasową metodą .withTimeZone(). Zmienia się strefa, ale moment w czasie pozostaje ten sam.
const warsawTime = Temporal.ZonedDateTime
.from('2025-11-03T14:00:00+01:00[Europe/Warsaw]')
warsawTime.withTimeZone('Asia/Tokyo')
// => 2025-11-03T22:00:00+09:00[Asia/Tokyo] (ten sam moment, inna strefa)
Metoda .withPlainTime() podmienia część czasową. Dostępna dla PlainDateTime i ZonedDateTime.
Temporal.PlainDateTime
.from('2025-11-03T14:30')
.withPlainTime('09:00:00')
// => 2025-11-03T09:00:00
Operacje arytmetyczne
Możesz dodawać i odejmować czas za pomocą .add() i .subtract(). Obie metody zwracają nową instancję tego samego typu.
// Dodawanie
Temporal.PlainDateTime
.from('2025-01-01T10:00')
.add({ months: 1, days: 10 })
// => 2025-02-11T10:00:00
// Odejmowanie
Temporal.PlainDateTime
.from('2025-01-01T10:00')
.subtract({ years: 1, hours: 5 })
// => 2024-01-01T05:00:00
Aby obliczyć różnicę między dwoma datami, użyj .until() lub .since(). Obie zwracają obiekt Duration.
const startDate = Temporal.PlainDate.from('2025-01-01')
const endDate = Temporal.PlainDate.from('2025-01-15')
// Ile czasu UPŁYNIE od startDate do endDate?
startDate.until(endDate).days
// => 14
// Ile czasu UPŁYNĘŁO od endDate do startDate? (w przeszłość = ujemna)
startDate.since(endDate).days
// => -14
Zaokrąglanie
Metoda .round() zaokrągla datę do określonej precyzji. Przydatne, gdy chcesz pominąć sekundy lub zaokrąglić do najbliższej godziny.
Temporal.PlainDateTime
.from('2025-10-15T14:37:45')
.round({ smallestUnit: 'minute' })
// => 2025-10-15T14:38:00
Temporal.PlainDateTime
.from('2025-10-15T14:37:45')
.round({ smallestUnit: 'hour' })
// => 2025-10-15T15:00:00
Porównywanie
Do porównywania dat służy statyczna metoda .compare() lub .equals() dla sprawdzenia równości.
const date = Temporal.PlainDate.from('2025-10-15')
const futureDate = Temporal.PlainDate.from('2025-11-01')
// compare() zwraca -1, 0 lub 1 (jak w Array.sort)
Temporal.PlainDate.compare(date, futureDate)
// => -1 (wcześniejsza)
Temporal.PlainDate.compare(futureDate, date)
// => 1 (późniejsza)
// equals() sprawdza równość
date.equals(futureDate)
// => false
date.equals(Temporal.PlainDate.from('2025-10-15'))
// => true
Serializacja
Do serializacji służą trzy metody:
const dt = Temporal.PlainDateTime.from('2025-10-15T14:38:00')
// toString() - format ISO
dt.toString()
// => 2025-10-15T14:38:00
// toJSON() - dla JSON.stringify (identyczne z `toString()`)
JSON.stringify({ date: dt })
// => {"date":"2025-10-15T14:38:00"}
// toLocaleString() - format lokalny
dt.toLocaleString('pl-PL')
// => 15.10.2025, 14:38:00
dt.toLocaleString('en-US')
// => 10/15/2025, 2:38:00 PM
Ważne: Temporal celowo blokuje porównania operatorami <, >, <=, >=. Zamiast tego, użyj .compare().
// ❌ To nie zadziała
if (date1 > date2) { }
// => TypeError: can't convert PlainDate to primitive type
// ✅ Użyj compare()
if (Temporal.PlainDate.compare(date1, date2) > 0) { }
Praca z UTC i strefami czasowymi
Temporal jawnie rozróżnia czas lokalny od UTC. To eliminuje problem Date, gdzie nigdy nie byłeś pewien, w jakiej strefie jesteś.
// Stworzenie timestamp w UTC
const utcNow = Temporal.Now.instant()
// => 2025-11-03T13:30:00Z
// Konwersja UTC na konkretną strefę czasową
utcNow.toZonedDateTimeISO('Europe/Warsaw')
// => 2025-11-03T14:30:00+01:00[Europe/Warsaw]
utcNow.toZonedDateTimeISO('America/New_York')
// => 2025-11-03T08:30:00-05:00[America/New_York]
// Konwersja lokalnego czasu na UTC
const localTime = Temporal.ZonedDateTime.from(
'2025-11-03T14:30:00+01:00[Europe/Warsaw]'
)
localTime.toInstant()
// => 2025-11-03T13:30:00Z
Dostęp do komponentów daty
Każdy obiekt Temporal daje bezpośredni dostęp do swoich komponentów.
const date = Temporal.PlainDateTime.from('2025-11-03T18:38:29.013')
// Podstawowe komponenty
date.year // => 2025
date.month // => 11
date.day // => 3
date.hour // => 18
date.minute // => 38
date.second // => 29
date.millisecond // => 13
// Informacje o dniu
date.dayOfWeek // => 1 (poniedziałek, 1 = poniedziałek, 7 = niedziela)
date.dayOfYear // => 307 (307. dzień roku)
// Informacje o tygodniu
date.weekOfYear // => 45 (45. tydzień roku)
date.yearOfWeek // => 2025 (rok do którego należy tydzień)
date.daysInWeek // => 7
// Informacje o miesiącu
date.daysInMonth // => 30 (listopad ma 30 dni)
date.monthsInYear // => 12
// Informacje o roku
date.inLeapYear // => false (2025 nie jest przestępny)
date.daysInYear // => 365 (365 dni w 2025)
Pełna lista właściwości w dokumentacji MDN.
Wydajność
Date jest szybszy od Temporal. Benchmark który przygotowałem pokazuje konkretne różnice:
- Tworzenie instancji:
Date~1.8x szybszy (18M vs 10M ops/sec z aktualnej daty, 16M vs 8M ops/sec ze stringa) - Obliczanie różnicy:
Date~1.4x szybszy (4.8M vs 3.3M ops/sec) - Serializacja:
Date~2.2x szybszy (9.7M vs 4.3M ops/sec) - Pobieranie komponentów:
Date~2x szybszy (11.9M vs 5.8M ops/sec)
Czy to ma znaczenie? Tylko jeśli wykonujesz dziesiątki tysięcy operacji na datach w krytycznej ścieżce. Dla typowych aplikacji webowych różnica jest nieodczuwalna - mówimy o mikrosekundach.
Jeśli wydajność jest kluczowa w Twoim przypadku, wykonaj własne testy z rzeczywistym obciążeniem. Dla 99% aplikacji zyski z czytelności i poprawności kodu Temporal przeważają nad drobnymi różnicami w wydajności.
Temporal w praktyce
Teorię mamy za sobą. Czas na przykłady z życia wzięte. Zobaczmy, jak wykorzystać Temporal w rzeczywistych scenariuszach.
Konwersja Date do Temporal
Metoda .toTemporalInstant() konwertuje Date do Temporal.Instant. Przydatne przy migracji istniejącego kodu lub pracy z bibliotekami używającymi Date.
const temporal = new Date().toTemporalInstant()
temporal.toZonedDateTimeISO("Europe/Warsaw")
// => 2025-11-03T15:31:37.587+01:00[Europe/Warsaw]
Sortowanie dat
const dates = [
Temporal.PlainDate.from('2025-12-25'),
Temporal.PlainDate.from('2025-01-01'),
Temporal.PlainDate.from('2025-07-04'),
]
dates.sort(Temporal.PlainDate.compare)
// => [2025-01-01, 2025-07-04, 2025-12-25]
Obliczanie kwartałów
function getQuarter(date) {
const plainDate = Temporal.PlainDate.from(date)
return Math.ceil(plainDate.month / 3)
}
function getQuarterStartDate(date) {
const plainDate = Temporal.PlainDate.from(date)
const quarter = getQuarter(date)
const startMonth = (quarter - 1) * 3 + 1
return plainDate.with({ month: startMonth, day: 1 })
}
function getQuarterEndDate(date) {
const plainDate = Temporal.PlainDate.from(date)
const quarter = getQuarter(date)
const endMonth = quarter * 3
const lastDay = Temporal.PlainDate
.from({ year: plainDate.year, month: endMonth, day: 1 })
.daysInMonth
return plainDate.with({ month: endMonth, day: lastDay })
}
getQuarter('2025-11-03')
// => 4
getQuarterStartDate('2025-11-03').toString()
// => 2025-10-01
getQuarterEndDate('2025-11-03').toString()
// => 2025-12-31
Ustawienie właściwości elementu input[type=date]
const datePicker = document.querySelector('input[type="date"]')
const today = Temporal.Now.plainDateISO()
// Przeglądarka za nas zserializuje Temporal
datePicker.max = today
datePicker.value = today
Czytelne definicje czasu
const timeout = Temporal.Duration.from({ hours: 5 }).total('milliseconds')
setTimeout(callback, timeout)
Porównywanie dat z różnymi strefami czasowymi
const nyTime = Temporal.ZonedDateTime.from('2025-01-01T12:00:00[America/New_York]')
const tokyoTime = Temporal.ZonedDateTime.from('2025-01-02T02:00:00[Asia/Tokyo]')
// To ten sam moment w czasie.
Temporal.ZonedDateTime.compare(nyTime, tokyoTime)
// => 0
// Ale różne lokalne czasy.
nyTime.hour
// => 12
tokyoTime.hour
// => 2
Lata przestępne
// 2024 to rok przestępny
const leapYear = Temporal.PlainDate.from('2024-02-29')
leapYear.inLeapYear
// => true
// Dodanie roku spowoduje przeskoczenie na 28 lutego
const nextYear = leapYear.add({ years: 1 })
nextYear.toString()
// => 2025-02-28
// Możesz kontrolować zachowanie i rzucić wyjątek
const nextYearStrict = leapYear.add({ years: 1 }, { overflow: 'reject' })
// => RangeError: date value "day" not in 1..28: 29
Obliczanie wieku użytkownika
function calculateAge(birthDate) {
const today = Temporal.Now.plainDateISO()
const birth = Temporal.PlainDate.from(birthDate)
const ageDuration = birth.until(today, { largestUnit: 'year' })
return ageDuration.years
}
calculateAge('1990-01-01')
// => 35
// Sprawdzanie pełnoletności
function isAdult(birthDate) {
return calculateAge(birthDate) >= 18
}
isAdult('2010-01-01')
// => false
isAdult('2000-01-01')
// => 25
Obliczanie dni roboczych
function countBusinessDays(startDate, endDate) {
let current = Temporal.PlainDate.from(startDate)
const end = Temporal.PlainDate.from(endDate)
let count = 0
while (Temporal.PlainDate.compare(current, end) < 0) {
if (current.dayOfWeek < 6) {
count++;
}
current = current.add({ days: 1 })
}
return count
}
countBusinessDays('2025-11-03', '2025-11-10')
// => 5
Zarządzanie subskrypcjami
class Subscription {
constructor(startDate, durationMonths) {
this.startDate = Temporal.PlainDate.from(startDate)
this.durationMonths = durationMonths
}
get endDate() {
return this.startDate.add({ months: this.durationMonths })
}
isActive() {
const today = Temporal.Now.plainDateISO()
return Temporal.PlainDate.compare(today, this.endDate) < 0
}
daysUntilExpiry() {
const today = Temporal.Now.plainDateISO()
return today.until(this.endDate).days
}
shouldSendReminder() {
const daysLeft = this.daysUntilExpiry()
return daysLeft <= 7 && daysLeft > 0
}
}
const sub = new Subscription('2025-11-01', 3)
sub.endDate.toString()
// => 2026-02-01
sub.isActive()
// => true
sub.daysUntilExpiry()
// => 90
Cykliczne wydarzenia
function generateRecurringDates(startDate, occurrences, interval) {
const dates = []
let current = Temporal.PlainDate.from(startDate)
for (let i = 0; i < occurrences; i++) {
dates.push(current)
current = current.add(interval)
}
return dates
}
// Cotygodniowe spotkania
const weeklyMeetings = generateRecurringDates(
'2025-11-03',
10,
{ weeks: 1 }
)
// => [2025-11-03, 2025-11-10, 2025-11-17, ...]
// Comiesięczne faktury
const monthlyInvoices = generateRecurringDates(
'2025-01-01',
12,
{ months: 1 }
)
// => [2025-01-01, 2025-02-01, 2025-03-01, ...]
Mierzenie czasu odpowiedzi (SLA)
class SLAMonitor {
constructor(slaHours = 24) {
this.slaHours = slaHours
}
isWithinSLA(requestTime, responseTime) {
const request = Temporal.Instant.from(requestTime)
const response = Temporal.Instant.from(responseTime)
const duration = request.until(response)
const totalHours = duration.total('hours')
return totalHours <= this.slaHours
}
getResponseTime(requestTime, responseTime) {
const request = Temporal.Instant.from(requestTime)
const response = Temporal.Instant.from(responseTime)
return request.until(response)
}
getSLADeadline(requestTime) {
const request = Temporal.Instant.from(requestTime)
return request.add({ hours: this.slaHours })
}
}
const sla = new SLAMonitor(24)
sla.getResponseTime(
'2025-11-03T10:00:00Z',
'2025-11-03T15:00:00Z'
).total('hours')
// => 5
sla.isWithinSLA(
'2025-11-03T10:00:00Z',
'2025-11-03T15:00:00Z'
)
// => true (5 godzin)
sla.isWithinSLA(
'2025-11-03T10:00:00Z',
'2025-11-05T10:00:00Z'
)
// => false (48 godzin)
sla.getSLADeadline('2025-11-03T10:00:00Z').toString()
// => 2025-11-04T10:00:00Z
Walidacja dat w formularzach
function validateDateRange(startDate, endDate) {
const today = Temporal.Now.plainDateISO()
const start = Temporal.PlainDate.from(startDate)
const end = Temporal.PlainDate.from(endDate)
const errors = []
// Data nie może być w przeszłości
if (Temporal.PlainDate.compare(start, today) < 0) {
errors.push('Data początkowa nie może być w przeszłości')
}
// Data końcowa musi być po dacie początkowej
if (Temporal.PlainDate.compare(end, start) <= 0) {
errors.push('Data końcowa musi być po dacie początkowej')
}
// Maksymalny zakres np. 90 dni
const duration = start.until(end)
if (duration.days > 90) {
errors.push('Maksymalny zakres to 90 dni')
}
return {
valid: errors.length === 0,
errors
}
}
validateDateRange('2025-11-01', '2025-12-01')
// => { valid: true, errors: [] }
validateDateRange('2025-10-01', '2025-09-01')
// => {
// valid: false,
// errors: ['Data końcowa musi być po dacie początkowej']
// }
Bibliografia
- MDN Temporal
- JavaScript Temporal is coming
- GitHub - proposal-temporal
- Temporal proposal
- Temporal cookbook