Merekam Data yang Tidak Bergantung pada Lokasi dan Tanggal Parsial dengan Temporal

Fitur web: Temporal

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

  1. Pilih jenis Temporal.Plain* tersempit yang cocok dengan data Anda:
    • Gunakan Temporal.PlainDate.from() untuk tanggal kalender lengkap dan Temporal.PlainTime.from() untuk waktu jam dinding.
    • Gunakan Temporal.PlainDateTime.from() (atau plainDate.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 ke PlainDate dengan .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.
  2. Perhatikan parameter calendar saat membuat jenis Plain: ISO 8601 ('iso8601') adalah kalender default saat mengurai string ISO atau membuat dengan PlainDate.from({ year, month, day }). Saat membuat PlainMonthDay atau PlainYearMonth dari paket properti—atau saat menggunakan kalender non-ISO—teruskan calendar secara eksplisit (misalnya, { month: 10, day: 31, calendar: 'iso8601' }).
  3. Gunakan .add() dengan opsi overflow untuk interval kalender berulang: Memanggil plainDate.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 menampilkan RangeError, 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.PlainDate untuk tanggal kalender lengkap, Temporal.PlainTime untuk waktu jam dinding, Temporal.PlainDateTime untuk pasangan tanggal-waktu lokal tanpa zona, Temporal.PlainMonthDay untuk tanggal tahunan berulang, dan Temporal.PlainYearMonth untuk masa berlaku atau periode penagihan tahun-bulan.
  • JANGAN melakukan aritmetika langsung pada Temporal.PlainMonthDay; konversikan ke Temporal.PlainDate terlebih dahulu melalui .toPlainDate({ year }) karena durasi bulan bervariasi menurut tahun.
  • LAKUKAN penggunaan .add() pada Temporal.PlainDate untuk interval kalender berulang, dengan mengandalkan overflow: 'constrain' (default) untuk membatasi tanggal akhir bulan atau overflow: 'reject' untuk menampilkan RangeError pada tanggal yang tidak ada.
  • LAKUKAN penentuan calendar (misalnya, calendar: 'iso8601') saat membuat jenis PlainMonthDay atau Plain non-ISO dari objek.
  • JANGAN gunakan jenis Plain jika Anda perlu merepresentasikan momen tertentu dalam waktu fisik (instan). Gunakan Temporal.Instant atau Temporal.ZonedDateTime untuk log, stempel waktu peristiwa, atau penjadwalan lintas zona waktu.
  • JANGAN membuat objek Date dari string ISO yang dibuat secara manual (seperti `${date}T00:00:00Z`). Buat Date dari epochMilliseconds dari Temporal.Instant atau Temporal.ZonedDateTime, dan baca kembali dengan Temporal.Instant.fromEpochMilliseconds(date.getTime()) atau date.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 .day langsung 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 Temporal tidak 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();
})();