Date & Time #

Working with dates and times is one of the most bug-prone areas in programming — not just in TypeScript, but in almost every language. JavaScript’s built-in Date object (inherited by TypeScript) carries many historical quirks: zero-based months, an inconsistent constructor, poor timezone support, and surprising mutability. TypeScript doesn’t fix the weaknesses of Date itself, but its type system can help prevent a number of common mistakes — especially when you define branded types to distinguish local vs UTC dates, or timestamps vs durations. This article covers how to work with Date safely in TypeScript, the traps to avoid, and when to switch to more modern solutions.

Creating Date Objects #

There are four ways to create a Date object, each with different behavior:

// 1. No arguments — the current time
const sekarang: Date = new Date();

// 2. From a Unix timestamp (milliseconds since January 1, 1970 UTC)
const dariTimestamp: Date = new Date(1_746_576_000_000);

// 3. From an ISO 8601 string — the most recommended way
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. From numeric components — CAREFUL: the month is zero-based!
const dariKomponen: Date = new Date(2025, 4, 7, 15, 0, 0);
//                                        ↑ Month 4 = MAY (not April!)
//                         year, month(0-11), day, hour, minute, second

The Biggest Trap: Zero-Based Months #

This is the most common source of bugs when working with Date:

// ANTI-PATTERN: Entering the month as seen on a calendar
const ulangTahun = new Date(1995, 11, 25); // Not November! This is DECEMBER
//                                ^^^ 11 = December (0=Jan, 1=Feb, ..., 11=Dec)

// CORRECT: Use named constants for clarity
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);

// Or even better: use an ISO string to avoid ambiguity entirely
const hariNatalISO = new Date("2025-12-25"); // Clear, can't go wrong

Validating Date Objects #

The Date constructor doesn’t throw an error for invalid input — it produces an Invalid Date:

const tanggalSalah = new Date("bukan-tanggal");
console.log(tanggalSalah);              // Invalid Date
console.log(isNaN(tanggalSalah.getTime())); // true — the way to check for Invalid Date

// A helper function for safe parsing
function parseDate(input: string): Date | null {
  const tanggal = new Date(input);
  if (isNaN(tanggal.getTime())) {
    return null; // Return null for invalid input
  }
  return tanggal;
}

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

Getters — Reading Date Components #

The Date object provides two groups of getters: ones working in local time and ones working in UTC:

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

// LOCAL time getters (depend on the system timezone)
console.log(waktu.getFullYear());     // Local year
console.log(waktu.getMonth());        // Local month (0-11)
console.log(waktu.getDate());         // Local day of month (1-31)
console.log(waktu.getDay());          // Local day of week (0=Sunday, 6=Saturday)
console.log(waktu.getHours());        // Local hour (0-23)
console.log(waktu.getMinutes());      // Local minutes
console.log(waktu.getSeconds());      // Local seconds
console.log(waktu.getMilliseconds()); // Local milliseconds
console.log(waktu.getTime());         // Unix timestamp in ms (always UTC)
console.log(waktu.getTimezoneOffset()); // Timezone offset in minutes

// UTC getters — more consistent across servers
console.log(waktu.getUTCFullYear());  // UTC year
console.log(waktu.getUTCMonth());     // UTC month (0-11)
console.log(waktu.getUTCDate());      // UTC day of month
console.log(waktu.getUTCHours());     // UTC hour
console.log(waktu.getUTCMinutes());   // UTC minutes

Getter Reference Table #

Local GetterUTC GetterValue
getFullYear()getUTCFullYear()Year (4 digits)
getMonth()getUTCMonth()Month 0–11
getDate()getUTCDate()Day of month 1–31
getDay()getUTCDay()Day of week 0–6
getHours()getUTCHours()Hour 0–23
getMinutes()getUTCMinutes()Minutes 0–59
getSeconds()getUTCSeconds()Seconds 0–59
getTime()Unix timestamp (ms)

Setters — Changing Date Components #

Setters change the Date object in place (mutation). This is behavior to watch out for — Date is mutable:

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

// Setters change the original object
jadwalRapat.setHours(14);       // Move to 14:00
jadwalRapat.setMinutes(30);     // Becomes 14:30
jadwalRapat.setFullYear(2025);  // Year stays 2025

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

// ANTI-PATTERN: Modifying a Date used in many places
function tambahSatuHari(tanggal: Date): Date {
  tanggal.setDate(tanggal.getDate() + 1); // ✗ Changes the original object!
  return tanggal;
}

// CORRECT: Always create a new Date for calculation results
function tambahSatuHariAman(tanggal: Date): Date {
  const hasilBaru = new Date(tanggal); // Make a copy
  hasilBaru.setDate(hasilBaru.getDate() + 1);
  return hasilBaru; // Return the new one — the original stays unchanged
}

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

Formatting with Intl.DateTimeFormat #

Intl.DateTimeFormat is a modern API that produces locale-specific date formats — far more powerful than toLocaleString() for production needs:

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

// Full Indonesian format
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"

// Date-only format
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"

// Time-only format
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"

// Relative format (when it happened) — 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"

Reusable Format Functions #

// Helper functions for consistent date formatting across the application
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(); // Always 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"

Date Calculations #

Difference Between Two Dates #

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 days
console.log(`Selisih: ${selisihJam(mulai, selesai)} jam`);     // 154 hours
console.log(`Selisih: ${selisihMenit(mulai, selesai)} menit`); // 9240 minutes

Adding and Subtracting Durations #

// Immutable helpers for Date manipulation
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());   // May 14
console.log(tambahBulan(sekarang, 3).toISOString());  // August 2025
console.log(tambahTahun(sekarang, 1).toISOString());  // May 2026

Comparing Dates #

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: Comparing Dates with the == or === operators
// console.log(tgl1 === new Date("2025-05-07")); // false! Different objects
// CORRECT: Use getTime() for comparison

Timezone Handling #

Timezone is one of the most complex parts of Date & Time. JavaScript Date objects always store time in UTC internally, but display it in the local timezone:

// Always use ISO 8601 with an explicit timezone offset to avoid ambiguity
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
// Both represent the exact same moment

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

// How to display a time in a specific timezone
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
Avoid creating Date objects from strings without an explicit timezone for important dates. new Date("2025-05-07") is interpreted as UTC midnight, but new Date("2025-05-07T00:00:00") without an offset is interpreted as local time — different behavior that often causes off-by-one-day bugs, especially for applications running on servers with a different timezone than their users.

Branded Types for Date Type Safety #

TypeScript doesn’t distinguish between a Date representing a local date, UTC, or a timestamp. Branded types can help prevent accidental mixing:

// Branded types for various time representations
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);
}

// Usage — TypeScript prevents mixing types
const ts = toUnixTimestamp(new Date());      // UnixTimestampMs
const iso = toISODateString(new Date());     // ISODateString

// A function that only accepts ISO strings
function simpanKeDatabase(iso: ISODateString): void {
  console.log(`Menyimpan: ${iso}`);
}

simpanKeDatabase(iso);                       // ✓ ISODateString
// simpanKeDatabase(ts.toString());           // ✗ plain strings not accepted
// simpanKeDatabase("2025-05-07");            // ✗ plain strings not accepted

Serialization and Deserialization #

// Serializing a Date to JSON — automatically uses 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"}'

// Deserialization — JSON.parse does NOT automatically convert strings to Dates
const parsed = JSON.parse(json);
console.log(typeof parsed.dibuatPada); // "string" — not a Date!
// parsed.dibuatPada.getFullYear(); // ✗ Runtime error — strings don't have getFullYear

// CORRECT: Explicit conversion after parsing
const dipulihkan: Date = new Date(parsed.dibuatPada);
console.log(dipulihkan.getFullYear()); // 2025 ✓

// With a proper interface for API data
interface ResponseAPI {
  nama: string;
  dibuatPada: string; // String, not Date — because JSON doesn't know about Dates
}

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

When to Switch to Third-Party Libraries #

The built-in Date object is enough for basic needs. Consider libraries for more complex requirements:

// date-fns — a functional, tree-shakeable, very popular library
import { format, addDays, differenceInDays, isAfter, parseISO } from "date-fns";
import { id } from "date-fns/locale"; // Indonesian locale

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 (TC39 proposal — the future of JavaScript)
// Currently available via the @js-temporal/polyfill
// import { Temporal } from "@js-temporal/polyfill";
// const sekarang = Temporal.Now.plainDateTimeISO();
// const besok = sekarang.add({ days: 1 });
// — Immutable, timezone-aware, far better than Date

Comparison Table #

Use the built-in Date if:
  ✓ Simple operations (create, format, basic differences)
  ✓ No complex timezone needs
  ✓ You want zero dependencies
  ✓ Bundle size is sensitive

Consider date-fns if:
  ✓ Many date manipulation operations
  ✓ Need rich locale formatting
  ✓ The team is already familiar with functional APIs
  ✓ Tree-shaking matters for bundle size

Consider the Temporal API (polyfill) if:
  ✓ Need accurate timezone-aware computation
  ✓ Complex calendar operations
  ✓ Immutability is a priority
  ✓ Ready to depend on an experimental API

Summary #

  • Zero-based months are the most common trap — new Date(2025, 4, 7) is May 7 (not April); use the ISO string new Date("2025-05-07") to avoid ambiguity entirely.
  • Validate Date with isNaN(tanggal.getTime()) — the constructor doesn’t throw for invalid input, it produces an Invalid Date; always validate after parsing from external strings.
  • Date is mutable — setters change the original object; always make a copy with new Date(tanggal) before modifying if the original value is still needed.
  • Use getTime() for comparisons — the == and === operators compare object references, not time values; a.getTime() === b.getTime() is the correct way.
  • Always store and transmit time in UTC (ISO 8601) — use toISOString() for serialization; use Intl.DateTimeFormat with the timeZone option for user-facing display.
  • Avoid strings without an explicit timezone for important dates — new Date("2025-05-07") is UTC midnight, but new Date("2025-05-07T00:00:00") is local midnight; the different behavior can cause off-by-one-day bugs.
  • Intl.DateTimeFormat is stronger than toLocaleString() for production — it supports explicit timezone specification and is more consistent across browsers and Node.js.
  • Intl.RelativeTimeFormat for relative time like “kemarin”, “3 jam yang lalu” — no extra library needed for these cases.
  • Branded types like ISODateString and UnixTimestampMs help prevent mixing different time representations at the type-system level.
  • Consider date-fns for complex date manipulation — the library is tree-shakeable, functional, and supports the Indonesian locale well.

← Previous: Map   Next: Regex →

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