Zapisywanie danych chronologicznych, które powinny pozostać takie same niezależnie od lokalizacji użytkownika – takich jak daty urodzenia, codzienne alarmy, daty ważności kart kredytowych czy miesięczne cykle rozliczeniowe – było w przypadku starszego obiektu Date podatne na błędy. Date zawsze reprezentuje konkretny moment w czasie i jest wyświetlany w lokalnej strefie czasowej użytkownika, więc zapisanie daty takiej jak "1990-01-01" może spowodować, że użytkownicy na zachód od UTC zobaczą "1989-12-31", a modelowanie dat częściowych (np. daty ważności karty zawierającej tylko miesiąc) wymusza stosowanie dowolnych dni zastępczych, co powoduje błędy przepełnienia.
Interfejs API Temporal udostępnia rodzinę niezależnych od lokalizacji typów „Plain”, które nie zawierają przesunięcia strefy czasowej i reprezentują daty kalendarzowe oraz godziny zegarowe dokładnie tak, jak są odczytywane z kalendarza lub zegara:
| Typ | Co modeluje | Typowe przypadki użycia |
|---|---|---|
Temporal.PlainDate |
Pełna data kalendarzowa (year, month, day) |
daty urodzenia, daty wydarzeń, daty rozliczenia subskrypcji; |
Temporal.PlainTime |
Czas zegarowy (hour, minute, second, …) |
Codzienne alarmy (9:00 czasu lokalnego), godziny otwarcia sklepu |
Temporal.PlainDateTime |
Data kalendarzowa + godzina bez strefy czasowej | Pływające harmonogramy lokalne, łączące PlainDate i PlainTime przed dołączeniem strefy |
Temporal.PlainMonthDay |
Miesiąc i dzień bez roku | powtarzające się co roku urodziny, rocznice i stałe święta; |
Temporal.PlainYearMonth |
Rok i miesiąc bez dnia | Wygaśnięcie karty kredytowej (2027-12), miesięczne okresy rozliczeniowe |
Jak to zrobić?
- Wybierz najbardziej szczegółowy typ
Temporal.Plain*, który pasuje do Twoich danych:- Używaj
Temporal.PlainDate.from()w przypadku pełnych dat kalendarzowych iTemporal.PlainTime.from()w przypadku godzin. - Używaj
Temporal.PlainDateTime.from()(lubplainDate.toPlainDateTime(plainTime)) do łączenia daty kalendarzowej i godziny bez powiązania ze strefą czasową. - Użyj
Temporal.PlainMonthDay.from()w przypadku dat rocznych bez roku i przekonwertuj naPlainDatez.toPlainDate({ year })przed wykonaniem obliczeń daty. - W przypadku wartości roku i miesiąca używaj
Temporal.PlainYearMonth.from(), aby do obliczeń nigdy nie były używane dowolne numery dni.
- Używaj
- Podczas tworzenia typów Plain zwróć uwagę na parametr
calendar: ISO 8601 ('iso8601') to domyślny kalendarz podczas analizowania ciągów znaków ISO lub tworzenia za pomocąPlainDate.from({ year, month, day }). Podczas tworzenia obiektówPlainMonthDaylubPlainYearMonthz worków właściwości lub podczas pracy z kalendarzami innymi niż ISO przekazuj jawniecalendar(np.{ month: 10, day: 31, calendar: 'iso8601' }). - Używaj
.add()z opcjamioverfloww przypadku cyklicznych interwałów kalendarzowych: wywołanieplainDate.add({ months: 1 })domyślnie używa{ overflow: 'constrain' }, co powoduje, że daty końca miesiąca, takie jak 31 stycznia, są ograniczane do 28 lub 29 lutego. Przekaż{ overflow: 'reject' }, gdy nieprawidłowe daty docelowe powinny powodować błądRangeErrorzamiast ograniczenia.
Przykład: pełne daty, godziny i PlainDateTime
// 1. Parse a calendar date and a wall-clock time from input strings
const birthdate = Temporal.PlainDate.from('1990-01-01');
const reminderTime = Temporal.PlainTime.from('09:00');
// Combine into a location-agnostic PlainDateTime without string interpolation
const localAppointment = birthdate.toPlainDateTime(reminderTime);
// 2. Display without time zone conversion; outputs the same date in any time zone
console.log(birthdate.toLocaleString('en-GB'));
console.log(localAppointment.toString()); // "1990-01-01T09:00:00"
// 3. Read calendar fields directly from the Plain type. A PlainDate already *is*
// a calendar date, so there is no need for the formatToParts() workaround that
// legacy Date code uses to recover year/month/day from an instant.
const { year, month, day } = birthdate;
Przykład: daty częściowe (PlainMonthDay i PlainYearMonth)
// Annual recurring event (no year component)
// Include explicit calendar when constructing from an object
const annualHoliday = Temporal.PlainMonthDay.from({
month: 10,
day: 31,
calendar: 'iso8601',
});
const today = Temporal.Now.plainDateISO();
const isToday = annualHoliday.equals(today.toPlainMonthDay());
// Convert PlainMonthDay to a PlainDate with a specific year before doing arithmetic,
// because month lengths (such as February) depend on whether the year is a leap year.
const occurrenceThisYear = annualHoliday.toPlainDate({ year: today.year });
// Monthly expiration (no day component, preventing day-of-month overflow bugs)
const cardExpiry = Temporal.PlainYearMonth.from({
year: 2027,
month: 12,
calendar: 'iso8601',
});
const currentMonth = today.toPlainYearMonth();
const isExpired = Temporal.PlainYearMonth.compare(cardExpiry, currentMonth) < 0;
Przykład: cykliczne interwały miesięczne (overflow: 'constrain' i 'reject')
const billingStart = Temporal.PlainDate.from('2024-01-31');
// Default overflow: 'constrain' clamps Jan 31 + 1 month to the last valid day of February
const nextBilling = billingStart.add({ months: 1 }, { overflow: 'constrain' });
console.log(nextBilling.toString()); // "2024-02-29" (2024 is a leap year)
// Use overflow: 'reject' to throw a RangeError when the target day does not exist
try {
billingStart.add({ months: 1 }, { overflow: 'reject' });
} catch (err) {
// RangeError: day 31 does not exist in February
}
Przykład: współdziałanie ze starszą wersją Date
Starsza wersja Date to sygnatura czasowa (moment), a nie data kalendarzowa. Przechowywanie wartości "1990-01-01" jako Date wymusza wybranie jakiegoś momentu (zwykle północy czasu UTC), a odczytanie jej w strefie czasowej na zachód od UTC daje wynik "31/12/1989". Jeśli biblioteka lub interfejs API nadal wymagają typu Date, przeprowadź konwersję bezpośrednio za pomocą Temporal.Instant, zamiast ręcznie tworzyć ciągi znaków ISO:
// PlainDate -> Date: attach a time zone (time of day defaults to the start of
// that day) to get an Instant, then hand the epoch milliseconds to Date.
// 'UTC' is a deliberate choice and must be documented; any zone would do,
// but the stored instant depends on it.
const instant = birthdate.toZonedDateTime('UTC').toInstant();
const legacyDate = new Date(instant.epochMilliseconds);
// Date -> PlainDate: go back through Instant (legacyDate.toTemporalInstant()
// is equivalent), then project into an explicit time zone. The result depends
// on the zone, which is exactly the drift risk.
const viewedDate = Temporal.Instant.fromEpochMilliseconds(legacyDate.getTime())
.toZonedDateTimeISO('America/New_York')
.toPlainDate();
// Compare Plain types with .equals() (or Temporal.PlainDate.compare), not strings.
console.log(viewedDate.equals(birthdate)); // false: the calendar date drifted to 1989-12-31
Strategiczne wdrażanie i sprawdzone metody
- UŻYWAJ
Temporal.PlainDatew przypadku pełnych dat kalendarzowych,Temporal.PlainTimew przypadku godzin,Temporal.PlainDateTimew przypadku par data-godzina bez strefy,Temporal.PlainMonthDayw przypadku cyklicznych dat rocznych iTemporal.PlainYearMonthw przypadku dat ważności lub okresów rozliczeniowych w formacie rok-miesiąc. - NIE wykonuj działań arytmetycznych bezpośrednio na typie
Temporal.PlainMonthDay. Najpierw przekonwertuj go na typTemporal.PlainDateza pomocą funkcji.toPlainDate({ year }), ponieważ długość miesięcy różni się w zależności od roku. - UŻYWAJ
.add()wTemporal.PlainDatew przypadku cyklicznych przedziałów kalendarzowych, polegając naoverflow: 'constrain'(domyślnie) do ograniczania dat na koniec miesiąca luboverflow: 'reject'do zgłaszania błęduRangeErrorw przypadku nieistniejących dat. - OKREŚL
calendar(np.calendar: 'iso8601') podczas tworzenia typówPlainMonthDaylub zwykłych typów innych niż ISO z obiektów. - NIE używaj typów Plain, jeśli chcesz przedstawić konkretny moment w czasie fizycznym (chwilę). Używaj
Temporal.InstantlubTemporal.ZonedDateTimew przypadku logów, sygnatur czasowych zdarzeń lub planowania w różnych strefach czasowych. - NIE twórz obiektów
Datez ręcznie utworzonych ciągów ISO (takich jak`${date}T00:00:00Z`). Utwórz obiektDatezepochMillisecondsobiektuTemporal.InstantlubTemporal.ZonedDateTimei odczytaj go za pomocąTemporal.Instant.fromEpochMilliseconds(date.getTime())lubdate.toTemporalInstant(), aby założenie dotyczące strefy czasowej było jednoznaczne. - NIE używaj funkcji
Intl.DateTimeFormat.prototype.formatToParts()do wyodrębniania ani porównywania wartości roku, miesiąca lub dnia. Odczytaj.year,.monthi.daybezpośrednio z typu Plain i porównaj z.equals(). - UŻYWAJ nazw IANA do identyfikowania stref czasowych (np.
America/New_York), a nie stałego przesunięcia względem czasu UTC. Przesunięcia zmieniają się wraz z czasem letnim i przepisami, więc etykiety takie jak"UTC-5"stają się nieprawidłowe przez część roku. - PAMIĘTAJ, że obiekty
Temporalsą niezmienne. Metody takie jak.add(),.subtract()czy.with()zawsze zwracają nową instancję z nowym odwołaniem, zamiast mutować pierwotny obiekt na miejscu, dzięki czemu można je bezpiecznie przechowywać bezpośrednio w reaktywnym stanie interfejsu.
Strategia zastępcza
Zastępcze rozwiązania i obsługa Temporal w przeglądarkach
W przypadku przeglądarek, które nie obsługują jeszcze natywnego interfejsu API Temporal, użyj wykrywania funkcji i dynamicznie wczytaj temporal-polyfill (~15 KB min+gz).
Pamiętaj, że zaimportowanie temporal-polyfill nie powoduje automatycznego przypisania obiektu Temporal do zakresu globalnego. Przypisz go bezpośrednio, jeśli Twój kod opiera się na globalThis.Temporal.
// Check if Temporal is supported natively
(async () => {
if (typeof Temporal === 'undefined') {
// Load the polyfill conditionally
const module = await import('https://esm.sh/temporal-polyfill');
globalThis.Temporal = module.Temporal;
// Extend Date.prototype if needed
Date.prototype.toTemporalInstant = module.toTemporalInstant;
}
initializeApp();
})();