Date & Time #

Bekerja dengan tanggal dan waktu adalah salah satu area yang paling rawan bug dalam pemrograman — bukan hanya di TypeScript, tapi di hampir semua bahasa. Objek Date bawaan JavaScript (yang diwarisi TypeScript) menyimpan banyak kejanggalan bersejarah: bulan berbasis nol, konstruktor yang tidak konsisten, tidak ada dukungan zona waktu yang baik, dan mutabilitas yang mengejutkan. TypeScript tidak memperbaiki kelemahan Date itu sendiri, tapi sistem tipenya bisa membantu mencegah sejumlah kesalahan umum — terutama saat kamu mendefinisikan branded type untuk membedakan tanggal lokal vs UTC, atau timestamp vs durasi. Artikel ini membahas cara bekerja dengan Date secara aman di TypeScript, jebakan yang harus dihindari, dan kapan beralih ke solusi yang lebih modern.

Membuat Objek Date #

Ada empat cara membuat objek Date, masing-masing dengan perilaku yang berbeda:

// 1. Tanpa argumen — waktu saat ini
const sekarang: Date = new Date();

// 2. Dari Unix timestamp (milidetik sejak 1 Januari 1970 UTC)
const dariTimestamp: Date = new Date(1_746_576_000_000);

// 3. Dari string ISO 8601 — cara yang paling direkomendasikan
const dariISO: Date = new Date("2025-05-07T08:00:00Z");      // UTC
const dariISOLokal: Date = new Date("2025-05-07T15:00:00+07:00"); // WIB

// 4. Dari komponen numerik — HATI-HATI: bulan berbasis nol!
const dariKomponen: Date = new Date(2025, 4, 7, 15, 0, 0);
//                                        ↑ Bulan 4 = MEI (bukan April!)
//                         tahun, bulan(0-11), hari, jam, menit, detik

Jebakan Terbesar: Bulan Berbasis Nol #

Ini adalah sumber bug paling umum saat bekerja dengan Date:

// ANTI-PATTERN: Langsung masukkan bulan seperti yang dilihat kalender
const ulangTahun = new Date(1995, 11, 25); // Bukan November! Ini DESEMBER
//                                ^^^ 11 = Desember (0=Jan, 1=Feb, ..., 11=Des)

// BENAR: Gunakan konstanta bernama untuk memperjelas
const BULAN = {
  JANUARI: 0, FEBRUARI: 1, MARET: 2,   APRIL: 3,
  MEI: 4,     JUNI: 5,     JULI: 6,    AGUSTUS: 7,
  SEPTEMBER: 8, OKTOBER: 9, NOVEMBER: 10, DESEMBER: 11,
} as const;

const hariNatal = new Date(2025, BULAN.DESEMBER, 25);

// Atau lebih baik lagi: gunakan string ISO untuk menghindari ambiguitas sepenuhnya
const hariNatalISO = new Date("2025-12-25"); // Jelas, tidak bisa salah

Validasi Objek Date #

Konstruktor Date tidak melempar error untuk input yang tidak valid — ia menghasilkan Invalid Date:

const tanggalSalah = new Date("bukan-tanggal");
console.log(tanggalSalah);              // Invalid Date
console.log(isNaN(tanggalSalah.getTime())); // true — cara mengecek Invalid Date

// Fungsi helper untuk parsing yang aman
function parseDate(input: string): Date | null {
  const tanggal = new Date(input);
  if (isNaN(tanggal.getTime())) {
    return null; // Return null untuk input tidak valid
  }
  return tanggal;
}

const hasil = parseDate("2025-05-07");
if (hasil !== null) {
  console.log(`Tanggal valid: ${hasil.toISOString()}`);
} else {
  console.log("Format tanggal tidak valid");
}

Getter — Mengambil Komponen Tanggal #

Objek Date menyediakan dua kelompok getter: yang bekerja dalam waktu lokal dan yang bekerja dalam UTC:

const waktu = new Date("2025-05-07T15:30:45.123Z"); // UTC

// Getter waktu LOKAL (bergantung pada timezone sistem)
console.log(waktu.getFullYear());     // Tahun lokal
console.log(waktu.getMonth());        // Bulan lokal (0-11)
console.log(waktu.getDate());         // Tanggal lokal (1-31)
console.log(waktu.getDay());          // Hari dalam minggu lokal (0=Minggu, 6=Sabtu)
console.log(waktu.getHours());        // Jam lokal (0-23)
console.log(waktu.getMinutes());      // Menit lokal
console.log(waktu.getSeconds());      // Detik lokal
console.log(waktu.getMilliseconds()); // Milidetik lokal
console.log(waktu.getTime());         // Unix timestamp milidetik (selalu UTC)
console.log(waktu.getTimezoneOffset()); // Offset timezone dalam menit

// Getter waktu UTC — lebih konsisten lintas server
console.log(waktu.getUTCFullYear());  // Tahun UTC
console.log(waktu.getUTCMonth());     // Bulan UTC (0-11)
console.log(waktu.getUTCDate());      // Tanggal UTC
console.log(waktu.getUTCHours());     // Jam UTC
console.log(waktu.getUTCMinutes());   // Menit UTC

Tabel Referensi Getter #

Getter LokalGetter UTCNilai
getFullYear()getUTCFullYear()Tahun (4 digit)
getMonth()getUTCMonth()Bulan 0–11
getDate()getUTCDate()Hari dalam bulan 1–31
getDay()getUTCDay()Hari dalam minggu 0–6
getHours()getUTCHours()Jam 0–23
getMinutes()getUTCMinutes()Menit 0–59
getSeconds()getUTCSeconds()Detik 0–59
getTime()Unix timestamp (ms)

Setter — Mengubah Komponen Tanggal #

Setter mengubah objek Date secara in-place (mutasi). Ini adalah perilaku yang perlu diwaspadai — Date bersifat mutable:

const jadwalRapat = new Date("2025-05-07T09:00:00");

// Setter mengubah objek asli
jadwalRapat.setHours(14);       // Pindahkan ke jam 14:00
jadwalRapat.setMinutes(30);     // Menjadi 14:30
jadwalRapat.setFullYear(2025);  // Tahun tetap 2025

console.log(jadwalRapat.toISOString()); // "2025-05-07T07:30:00.000Z" (dalam UTC)

// ANTI-PATTERN: Memodifikasi Date yang digunakan di banyak tempat
function tambahSatuHari(tanggal: Date): Date {
  tanggal.setDate(tanggal.getDate() + 1); // ✗ Mengubah objek asli!
  return tanggal;
}

// BENAR: Selalu buat Date baru untuk hasil perhitungan
function tambahSatuHariAman(tanggal: Date): Date {
  const hasilBaru = new Date(tanggal); // Buat salinan
  hasilBaru.setDate(hasilBaru.getDate() + 1);
  return hasilBaru; // Kembalikan yang baru — asli tidak berubah
}

const hari1 = new Date("2025-05-07");
const hari2 = tambahSatuHariAman(hari1);
console.log(hari1.toISOString()); // 2025-05-07 — tidak berubah
console.log(hari2.toISOString()); // 2025-05-08

Pemformatan dengan Intl.DateTimeFormat #

Intl.DateTimeFormat adalah API modern yang menghasilkan format tanggal sesuai locale — jauh lebih powerful dari toLocaleString() untuk kebutuhan produksi:

const tanggal = new Date("2025-05-07T15:30:00+07:00");

// Format Indonesia lengkap
const formatIndonesia = new Intl.DateTimeFormat("id-ID", {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric",
  hour: "2-digit",
  minute: "2-digit",
  second: "2-digit",
  timeZone: "Asia/Jakarta",
});
console.log(formatIndonesia.format(tanggal));
// "Rabu, 7 Mei 2025 pukul 15.30.00"

// Format tanggal saja
const formatTanggal = new Intl.DateTimeFormat("id-ID", {
  year: "numeric",
  month: "2-digit",
  day: "2-digit",
  timeZone: "Asia/Jakarta",
});
console.log(formatTanggal.format(tanggal)); // "07/05/2025"

// Format waktu saja
const formatWaktu = new Intl.DateTimeFormat("id-ID", {
  hour: "2-digit",
  minute: "2-digit",
  timeZone: "Asia/Jakarta",
  hour12: false,
});
console.log(formatWaktu.format(tanggal)); // "15.30"

// Format relatif (kapan terjadinya) — Intl.RelativeTimeFormat
const relativeFormat = new Intl.RelativeTimeFormat("id-ID", { numeric: "auto" });
console.log(relativeFormat.format(-1, "day"));  // "kemarin"
console.log(relativeFormat.format(1, "day"));   // "besok"
console.log(relativeFormat.format(-3, "hour")); // "3 jam yang lalu"
console.log(relativeFormat.format(2, "week"));  // "dalam 2 minggu"

Fungsi Format yang Reusable #

// Fungsi helper untuk format tanggal yang konsisten di seluruh aplikasi
const TIMEZONE_WIB = "Asia/Jakarta";
const LOCALE_ID = "id-ID";

function formatTanggalIndonesia(tanggal: Date): string {
  return new Intl.DateTimeFormat(LOCALE_ID, {
    day: "numeric",
    month: "long",
    year: "numeric",
    timeZone: TIMEZONE_WIB,
  }).format(tanggal);
}

function formatWaktuSingkat(tanggal: Date): string {
  return new Intl.DateTimeFormat(LOCALE_ID, {
    hour: "2-digit",
    minute: "2-digit",
    timeZone: TIMEZONE_WIB,
    hour12: false,
  }).format(tanggal);
}

function formatISO(tanggal: Date): string {
  return tanggal.toISOString(); // Selalu UTC, format: "2025-05-07T08:30:00.000Z"
}

const acara = new Date("2025-05-07T08:30:00Z");
console.log(formatTanggalIndonesia(acara)); // "7 Mei 2025"
console.log(formatWaktuSingkat(acara));     // "15.30" (WIB = UTC+7)
console.log(formatISO(acara));             // "2025-05-07T08:30:00.000Z"

Perhitungan Tanggal #

Selisih antara Dua Tanggal #

const MILIDETIK_PER_DETIK = 1_000;
const MILIDETIK_PER_MENIT = 60 * MILIDETIK_PER_DETIK;
const MILIDETIK_PER_JAM   = 60 * MILIDETIK_PER_MENIT;
const MILIDETIK_PER_HARI  = 24 * MILIDETIK_PER_JAM;

function selisihHari(awal: Date, akhir: Date): number {
  const selisihMs = akhir.getTime() - awal.getTime();
  return Math.round(selisihMs / MILIDETIK_PER_HARI);
}

function selisihJam(awal: Date, akhir: Date): number {
  const selisihMs = akhir.getTime() - awal.getTime();
  return Math.round(selisihMs / MILIDETIK_PER_JAM);
}

function selisihMenit(awal: Date, akhir: Date): number {
  const selisihMs = akhir.getTime() - awal.getTime();
  return Math.round(selisihMs / MILIDETIK_PER_MENIT);
}

const mulai = new Date("2025-05-01T08:00:00Z");
const selesai = new Date("2025-05-07T18:00:00Z");

console.log(`Selisih: ${selisihHari(mulai, selesai)} hari`);   // 6 hari
console.log(`Selisih: ${selisihJam(mulai, selesai)} jam`);     // 154 jam
console.log(`Selisih: ${selisihMenit(mulai, selesai)} menit`); // 9240 menit

Menambah dan Mengurangi Durasi #

// Helper immutable untuk manipulasi Date
function tambahDetik(tanggal: Date, detik: number): Date {
  return new Date(tanggal.getTime() + detik * MILIDETIK_PER_DETIK);
}

function tambahMenit(tanggal: Date, menit: number): Date {
  return new Date(tanggal.getTime() + menit * MILIDETIK_PER_MENIT);
}

function tambahJam(tanggal: Date, jam: number): Date {
  return new Date(tanggal.getTime() + jam * MILIDETIK_PER_JAM);
}

function tambahHari(tanggal: Date, hari: number): Date {
  return new Date(tanggal.getTime() + hari * MILIDETIK_PER_HARI);
}

function tambahBulan(tanggal: Date, bulan: number): Date {
  const hasil = new Date(tanggal);
  hasil.setMonth(hasil.getMonth() + bulan);
  return hasil;
}

function tambahTahun(tanggal: Date, tahun: number): Date {
  const hasil = new Date(tanggal);
  hasil.setFullYear(hasil.getFullYear() + tahun);
  return hasil;
}

const sekarang = new Date("2025-05-07T12:00:00Z");

console.log(tambahJam(sekarang, 3).toISOString());    // 15:00 UTC
console.log(tambahHari(sekarang, 7).toISOString());   // 14 Mei
console.log(tambahBulan(sekarang, 3).toISOString());  // Agustus 2025
console.log(tambahTahun(sekarang, 1).toISOString());  // Mei 2026

Perbandingan Tanggal #

function sebelum(a: Date, b: Date): boolean {
  return a.getTime() < b.getTime();
}

function sesudah(a: Date, b: Date): boolean {
  return a.getTime() > b.getTime();
}

function samaPersis(a: Date, b: Date): boolean {
  return a.getTime() === b.getTime();
}

function dalamRentang(tanggal: Date, awal: Date, akhir: Date): boolean {
  return tanggal.getTime() >= awal.getTime() && tanggal.getTime() <= akhir.getTime();
}

const tgl1 = new Date("2025-05-07");
const tgl2 = new Date("2025-06-01");
const tgl3 = new Date("2025-05-15");

console.log(sebelum(tgl1, tgl2));              // true
console.log(dalamRentang(tgl3, tgl1, tgl2));  // true

// ANTI-PATTERN: Membandingkan Date dengan operator == atau ===
// console.log(tgl1 === new Date("2025-05-07")); // false! Objek berbeda
// BENAR: Gunakan getTime() untuk perbandingan

Penanganan Timezone #

Timezone adalah salah satu bagian paling kompleks dari Date & Time. Objek Date JavaScript selalu menyimpan waktu dalam UTC secara internal, tapi menampilkannya dalam timezone lokal:

// Selalu gunakan ISO 8601 dengan offset timezone eksplisit untuk menghindari ambiguitas
const waktuWIB = new Date("2025-05-07T15:00:00+07:00");   // 15:00 WIB
const waktuUTC = new Date("2025-05-07T08:00:00Z");         // 08:00 UTC
// Keduanya merepresentasikan momen yang persis sama

console.log(waktuWIB.getTime() === waktuUTC.getTime()); // true

// Cara menampilkan waktu dalam timezone spesifik
function formatDalamTimezone(tanggal: Date, timezone: string): string {
  return new Intl.DateTimeFormat("id-ID", {
    timeZone: timezone,
    year: "numeric", month: "2-digit", day: "2-digit",
    hour: "2-digit", minute: "2-digit", second: "2-digit",
    hour12: false,
  }).format(tanggal);
}

const momenSama = new Date("2025-05-07T08:00:00Z");
console.log(formatDalamTimezone(momenSama, "Asia/Jakarta"));   // WIB (UTC+7)
console.log(formatDalamTimezone(momenSama, "Asia/Makassar"));  // WITA (UTC+8)
console.log(formatDalamTimezone(momenSama, "Asia/Jayapura")); // WIT (UTC+9)
console.log(formatDalamTimezone(momenSama, "UTC"));            // UTC
Hindari membuat Date dari string tanpa timezone eksplisit untuk tanggal yang penting. new Date("2025-05-07") diinterpretasikan sebagai UTC tengah malam, tapi new Date("2025-05-07T00:00:00") tanpa offset diinterpretasikan sebagai waktu lokal — perilaku yang berbeda dan sering menyebabkan bug off-by-one-day, terutama untuk aplikasi yang dijalankan di server dengan timezone berbeda dari pengguna.

Branded Type untuk Keamanan Tipe Tanggal #

TypeScript tidak membedakan antara Date yang merepresentasikan tanggal lokal, UTC, atau timestamp. Branded type bisa membantu mencegah pencampuran yang tidak disengaja:

// Branded types untuk berbagai representasi waktu
type ISODateString = string & { readonly __brand: "ISODateString" };
type UnixTimestampMs = number & { readonly __brand: "UnixTimestampMs" };
type LocaleDateString = string & { readonly __brand: "LocaleDateString" };

// Constructor functions
function toISODateString(tanggal: Date): ISODateString {
  return tanggal.toISOString() as ISODateString;
}

function toUnixTimestamp(tanggal: Date): UnixTimestampMs {
  return tanggal.getTime() as UnixTimestampMs;
}

function fromUnixTimestamp(ts: UnixTimestampMs): Date {
  return new Date(ts);
}

// Penggunaan — TypeScript mencegah pencampuran tipe
const ts = toUnixTimestamp(new Date());      // UnixTimestampMs
const iso = toISODateString(new Date());     // ISODateString

// Fungsi yang hanya menerima ISO string
function simpanKeDatabase(iso: ISODateString): void {
  console.log(`Menyimpan: ${iso}`);
}

simpanKeDatabase(iso);                       // ✓ ISODateString
// simpanKeDatabase(ts.toString());           // ✗ string biasa tidak diterima
// simpanKeDatabase("2025-05-07");            // ✗ string biasa tidak diterima

Serialisasi dan Deserialisasi #

// Serialisasi Date ke JSON — otomatis menggunakan ISO 8601 UTC
const data = {
  nama: "Budi",
  dibuatPada: new Date("2025-05-07T08:00:00Z"),
};

const json = JSON.stringify(data);
// '{"nama":"Budi","dibuatPada":"2025-05-07T08:00:00.000Z"}'

// Deserialisasi — JSON.parse TIDAK otomatis mengubah string ke Date
const parsed = JSON.parse(json);
console.log(typeof parsed.dibuatPada); // "string" — bukan Date!
// parsed.dibuatPada.getFullYear(); // ✗ Error di runtime — string tidak punya getFullYear

// BENAR: Konversi eksplisit setelah parsing
const dipulihkan: Date = new Date(parsed.dibuatPada);
console.log(dipulihkan.getFullYear()); // 2025 ✓

// Dengan interface yang tepat untuk data dari API
interface ResponseAPI {
  nama: string;
  dibuatPada: string; // String, bukan Date — karena JSON tidak kenal Date
}

function prosesResponse(raw: ResponseAPI): { nama: string; dibuatPada: Date } {
  return {
    nama: raw.nama,
    dibuatPada: new Date(raw.dibuatPada), // Konversi eksplisit
  };
}

Kapan Beralih ke Library Pihak Ketiga #

Objek Date bawaan cukup untuk kebutuhan dasar. Pertimbangkan library untuk kebutuhan yang lebih kompleks:

// date-fns — library fungsional, tree-shakeable, sangat populer
import { format, addDays, differenceInDays, isAfter, parseISO } from "date-fns";
import { id } from "date-fns/locale"; // Locale Indonesia

const sekarang = new Date();
const besok = addDays(sekarang, 1);

console.log(format(sekarang, "EEEE, d MMMM yyyy", { locale: id }));
// "Rabu, 7 Mei 2025"

console.log(differenceInDays(besok, sekarang)); // 1
console.log(isAfter(besok, sekarang));           // true

// Temporal API (proposal TC39 — masa depan JavaScript)
// Saat ini tersedia via polyfill @js-temporal/polyfill
// import { Temporal } from "@js-temporal/polyfill";
// const sekarang = Temporal.Now.plainDateTimeISO();
// const besok = sekarang.add({ days: 1 });
// — Immutable, timezone-aware, jauh lebih baik dari Date

Tabel Perbandingan #

Gunakan Date bawaan jika:
  ✓ Operasi sederhana (buat, format, selisih dasar)
  ✓ Tidak perlu timezone yang kompleks
  ✓ Ingin zero dependency
  ✓ Bundle size sensitif

Pertimbangkan date-fns jika:
  ✓ Banyak operasi manipulasi tanggal
  ✓ Perlu format locale yang kaya
  ✓ Tim sudah familiar dengan API fungsional
  ✓ Tree-shaking penting untuk bundle size

Pertimbangkan Temporal API (polyfill) jika:
  ✓ Butuh timezone-aware computation yang akurat
  ✓ Operasi kalender yang kompleks
  ✓ Immutability adalah prioritas
  ✓ Siap bergantung pada API eksperimental

Ringkasan #

  • Bulan berbasis nol adalah jebakan paling umum — new Date(2025, 4, 7) adalah 7 Mei (bukan April); gunakan string ISO new Date("2025-05-07") untuk menghindari ambiguitas sepenuhnya.
  • Validasi Date dengan isNaN(tanggal.getTime()) — konstruktor tidak melempar error untuk input invalid, melainkan menghasilkan Invalid Date; selalu validasi setelah parsing dari string eksternal.
  • Date bersifat mutable — setter mengubah objek asli; selalu buat salinan dengan new Date(tanggal) sebelum memodifikasi jika nilai asli masih dibutuhkan.
  • Gunakan getTime() untuk perbandingan — operator == dan === membandingkan referensi objek, bukan nilai waktu; a.getTime() === b.getTime() adalah cara yang benar.
  • Selalu simpan dan transmisikan waktu dalam UTC (ISO 8601) — gunakan toISOString() untuk serialisasi; gunakan Intl.DateTimeFormat dengan opsi timeZone untuk tampilan ke pengguna.
  • Hindari string tanpa timezone eksplisit untuk tanggal penting — new Date("2025-05-07") adalah UTC midnight, tapi new Date("2025-05-07T00:00:00") adalah lokal midnight; perilaku berbeda bisa menyebabkan bug off-by-one-day.
  • Intl.DateTimeFormat lebih kuat dari toLocaleString() untuk produksi — ia mendukung spesifikasi timezone eksplisit dan lebih konsisten lintas browser dan Node.js.
  • Intl.RelativeTimeFormat untuk waktu relatif seperti “kemarin”, “3 jam yang lalu” — tidak perlu library tambahan untuk kasus ini.
  • Branded type seperti ISODateString dan UnixTimestampMs membantu mencegah pencampuran representasi waktu yang berbeda di level type system.
  • Pertimbangkan date-fns untuk manipulasi tanggal yang kompleks — library ini tree-shakeable, fungsional, dan mendukung locale Indonesia dengan baik.

← Sebelumnya: Map   Berikutnya: Regex →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact