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