Mencatat data kronologis yang harus tetap sama terlepas dari lokasi pelihat—seperti tanggal lahir, alarm harian, masa berlaku kartu kredit, atau siklus penagihan bulanan—secara historis rentan terhadap error dengan objek Date lama. Karena Date selalu mewakili waktu tertentu dan ditampilkan menggunakan zona waktu lokal pelihat, menyimpan tanggal seperti "1990-01-01" dapat menyebabkan pengguna di sebelah barat UTC melihat "1989-12-31", sedangkan pemodelan tanggal parsial (seperti masa berlaku kartu khusus bulan) memaksa hari placeholder arbitrer yang menyebabkan bug tambahan.
Temporal API menyediakan kelompok jenis "Plain" yang tidak bergantung pada lokasi, yang tidak memiliki selisih zona waktu dan mewakili tanggal kalender dan waktu jam dinding persis seperti yang dibaca dari kalender atau jam:
| Jenis | Yang Dimodelkan | Kasus Penggunaan Umum |
|---|---|---|
Temporal.PlainDate |
Tanggal kalender lengkap (year, month, day) |
Tanggal lahir, tanggal acara, tanggal penagihan langganan |
Temporal.PlainTime |
Waktu sebenarnya (hour, minute, second, ...) |
Alarm harian (pukul 09.00 di mana pun pengguna berada), jam buka toko |
Temporal.PlainDateTime |
Tanggal kalender + waktu jam dinding tanpa zona waktu | Jadwal lokal mengambang, menggabungkan PlainDate dan PlainTime sebelum memasang zona |
Temporal.PlainMonthDay |
Bulan dan hari tanpa tahun | Ulang tahun, hari jadi, dan hari libur tetap tahunan berulang |
Temporal.PlainYearMonth |
Tahun dan bulan tanpa hari | Masa berlaku kartu kredit (2027-12), periode penagihan bulanan |
Cara Menerapkan
- Pilih jenis
Temporal.Plain*tersempit yang cocok dengan data Anda:- Gunakan
Temporal.PlainDate.from()untuk tanggal kalender lengkap danTemporal.PlainTime.from()untuk waktu jam dinding. - Gunakan
Temporal.PlainDateTime.from()(atauplainDate.toPlainDateTime(plainTime)) saat menggabungkan tanggal kalender dan waktu jam dinding tanpa mengikat ke zona waktu. - Gunakan
Temporal.PlainMonthDay.from()untuk tanggal tahunan tanpa tahun, dan konversikan kePlainDatedengan.toPlainDate({ year })sebelum melakukan aritmetika tanggal. - Gunakan
Temporal.PlainYearMonth.from()untuk nilai tahun dan bulan sehingga angka hari arbitrer tidak pernah bocor ke dalam penghitungan.
- Gunakan
- Perhatikan parameter
calendarsaat membuat jenis Plain: ISO 8601 ('iso8601') adalah kalender default saat mengurai string ISO atau membuat denganPlainDate.from({ year, month, day }). Saat membuatPlainMonthDayatauPlainYearMonthdari paket properti—atau saat menggunakan kalender non-ISO—teruskancalendarsecara eksplisit (misalnya,{ month: 10, day: 31, calendar: 'iso8601' }). - Gunakan
.add()dengan opsioverflowuntuk interval kalender berulang: MemanggilplainDate.add({ months: 1 })akan ditetapkan secara default ke{ overflow: 'constrain' }, yang membatasi tanggal akhir bulan seperti 31 Jan hingga 28/29 Feb. Teruskan{ overflow: 'reject' }jika tanggal target yang tidak valid akan menampilkanRangeError, bukan menjepit.
Contoh: Contoh: Tanggal Lengkap, Waktu Jam Dinding, dan 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;
Contoh: Tanggal Parsial (PlainMonthDay dan 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;
Contoh: Interval Bulanan Berulang (overflow: 'constrain' vs. '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
}
Contoh: Berinteroperasi dengan Date Lama
Date lama adalah stempel waktu (instan), bukan tanggal kalender. Menyimpan "1990-01-01" sebagai Date memaksa Anda memilih beberapa instan (biasanya tengah malam UTC), dan membacanya kembali di zona waktu barat UTC akan menghasilkan "31/12/1989". Jika library atau API masih memerlukan Date, konversikan secara eksplisit melalui Temporal.Instant, bukan membuat string ISO secara manual:
// 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
Penerapan Strategis & Praktik Terbaik
- GUNAKAN
Temporal.PlainDateuntuk tanggal kalender lengkap,Temporal.PlainTimeuntuk waktu jam dinding,Temporal.PlainDateTimeuntuk pasangan tanggal-waktu lokal tanpa zona,Temporal.PlainMonthDayuntuk tanggal tahunan berulang, danTemporal.PlainYearMonthuntuk masa berlaku atau periode penagihan tahun-bulan. - JANGAN melakukan aritmetika langsung pada
Temporal.PlainMonthDay; konversikan keTemporal.PlainDateterlebih dahulu melalui.toPlainDate({ year })karena durasi bulan bervariasi menurut tahun. - LAKUKAN penggunaan
.add()padaTemporal.PlainDateuntuk interval kalender berulang, dengan mengandalkanoverflow: 'constrain'(default) untuk membatasi tanggal akhir bulan atauoverflow: 'reject'untuk menampilkanRangeErrorpada tanggal yang tidak ada. - LAKUKAN penentuan
calendar(misalnya,calendar: 'iso8601') saat membuat jenisPlainMonthDayatau Plain non-ISO dari objek. - JANGAN gunakan jenis Plain jika Anda perlu merepresentasikan momen tertentu dalam waktu fisik (instan). Gunakan
Temporal.InstantatauTemporal.ZonedDateTimeuntuk log, stempel waktu peristiwa, atau penjadwalan lintas zona waktu. - JANGAN membuat objek
Datedari string ISO yang dibuat secara manual (seperti`${date}T00:00:00Z`). BuatDatedariepochMillisecondsdariTemporal.InstantatauTemporal.ZonedDateTime, dan baca kembali denganTemporal.Instant.fromEpochMilliseconds(date.getTime())ataudate.toTemporalInstant(), sehingga asumsi zona waktu menjadi eksplisit. - JANGAN gunakan
Intl.DateTimeFormat.prototype.formatToParts()untuk mengekstrak atau membandingkan nilai tahun, bulan, atau hari. Baca.year,.month, dan.daylangsung dari jenis Plain, dan bandingkan dengan.equals(). - LAKUKAN identifikasi zona waktu berdasarkan nama IANA (misalnya,
America/New_York) dan jangan pernah berdasarkan selisih UTC tetap. Selisih berubah seiring waktu musim panas dan legislasi, sehingga label seperti"UTC-5"menjadi salah untuk sebagian waktu dalam setahun. - LAKUKAN: ingat bahwa objek
Temporaltidak dapat diubah. Metode seperti.add(),.subtract(), atau.with()selalu menampilkan instance baru dengan referensi baru, bukan mengubah objek asli di tempat, sehingga aman untuk disimpan langsung dalam status UI reaktif.
Strategi Penggantian
Penggantian & dukungan browser untuk Temporal
Untuk browser yang belum mendukung Temporal API native, gunakan deteksi fitur dan muat temporal-polyfill secara dinamis (~15 KB min+gz).
Perhatikan bahwa mengimpor temporal-polyfill tidak otomatis menetapkan objek Temporal ke cakupan global. Tetapkan secara eksplisit jika kode Anda mengandalkan 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();
})();