Temporal — nowy standard dat i czasu w JavaScript

  • 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 Temporalniemutowalne. 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

Zobacz także