DEV Community

HK Lee
HK Lee

Posted on • Originally published at pockit.tools

A Deep Dive into JavaScript's Temporal API: Finally Escaping Date Hell in 2025

If you've ever written JavaScript that handles dates and times, you've experienced the pain. The confusion between UTC and local time. The month being zero-indexed (January is 0, not 1). The mutable Date object that can be accidentally modified anywhere in your codebase. The nightmare of timezone conversions. The dependency on libraries like moment.js or date-fns just to do basic operations that should be built into the language.

The JavaScript Date object has been broken since 1995, and we've been living with its quirks for nearly three decades. But finally, relief is here.

The Temporal API is a new, modern date/time API that's been in development since 2017 and has now reached Stage 3 in the TC39 process. It's shipping in browsers as we speak, with Chrome, Firefox, and Safari all actively implementing it. By mid-2025, it will be universally available.

In this comprehensive guide, we'll explore everything you need to know about Temporal: its core concepts, the problems it solves, practical code examples, and how to migrate from legacy solutions. Whether you're building a scheduling app, handling international users across timezones, or just want to stop worrying about date bugs, this guide has you covered.

Why the JavaScript Date Object is Fundamentally Broken

Before we dive into Temporal, let's understand why we needed a replacement in the first place. The Date object isn't just inconvenient—it's fundamentally flawed in ways that cause real bugs in production.

Problem 1: Mutability Creates Hidden Bugs

const meeting = new Date('2025-01-15T10:00:00');
scheduleMeeting(meeting);

// Somewhere else in your codebase...
meeting.setHours(meeting.getHours() + 2);
// The original meeting time has now been modified!
Enter fullscreen mode Exit fullscreen mode

This mutability means you can never trust a Date object that's been passed around your application. Any function could have modified it.

Problem 2: Zero-Indexed Months (The Classic)

// What month is this?
const date = new Date(2025, 1, 14);
console.log(date.toISOString()); // 2025-02-14, NOT January!

// Creating January 15th requires using 0
const january = new Date(2025, 0, 15);
Enter fullscreen mode Exit fullscreen mode

This has caused countless bugs. Every developer has written new Date(2025, 12, 25) hoping to get December 25th, only to get January 25th, 2026 instead.

Problem 3: Timezone Chaos

// What time is this?
const date = new Date('2025-01-15T10:00:00');
console.log(date.getHours()); // Depends on YOUR timezone!

// The same string creates different Date objects on different machines
// A server in UTC and a browser in PST will interpret this differently
Enter fullscreen mode Exit fullscreen mode

The Date object stores an absolute timestamp but displays it in the local timezone, mixing two completely different concepts. There's no way to represent "10 AM in Tokyo" without converting everything to UTC first.

Problem 4: Limited Arithmetic

// Add 1 month to January 31st
const jan31 = new Date(2025, 0, 31);
jan31.setMonth(jan31.getMonth() + 1);
console.log(jan31.toDateString()); // "Mon Mar 03 2025" - Wait, what?

// February doesn't have 31 days, so it "overflows" to March 3rd
Enter fullscreen mode Exit fullscreen mode

Date arithmetic with Date requires manual calculation and is full of edge cases. Want to add 2 weeks and 3 days? You need to calculate that in milliseconds.

Problem 5: Parsing is Unpredictable

// These produce different results across browsers:
new Date('2025-01-15'); // Parsed as UTC in some browsers, local in others
new Date('01/15/2025'); // US format? Or DD/MM/YYYY?
new Date('January 15, 2025'); // Works, but not portable
Enter fullscreen mode Exit fullscreen mode

The Date constructor's parsing is implementation-dependent and has caused countless production bugs.

Enter Temporal: A Complete Reimagining

The Temporal API addresses every single one of these problems with a well-designed, comprehensive solution. Let's explore its core types and concepts.

The Core Philosophy

Temporal is built on several key principles:

  1. Immutability: All Temporal objects are immutable. Operations return new objects.
  2. Explicit Types: Different types for different use cases (dates, times, timezones, durations).
  3. No Surprises: Months are 1-indexed. Parsing is strict. Behavior is consistent.
  4. Timezone-Aware by Design: Built-in support for IANA timezone database.
  5. Human-Friendly: APIs designed around how humans think about time.

The Temporal Namespace

All Temporal types live under the Temporal global namespace:

Temporal.PlainDate      // A date without time or timezone (2025-01-15)
Temporal.PlainTime      // A time without date or timezone (10:30:00)
Temporal.PlainDateTime  // Date + time, no timezone (2025-01-15T10:30:00)
Temporal.ZonedDateTime  // Date + time + timezone (2025-01-15T10:30:00[America/New_York])
Temporal.Instant        // An exact moment in time (like Date, but immutable)
Temporal.Duration       // A length of time (2 hours, 30 minutes)
Temporal.PlainYearMonth // Just year and month (2025-01)
Temporal.PlainMonthDay  // Just month and day (01-15, for recurring events)
Enter fullscreen mode Exit fullscreen mode

This type system immediately clarifies intent. When you see a Temporal.PlainDate, you know there's no hidden timezone. When you see a Temporal.ZonedDateTime, you know exactly which timezone it's in.

Part 1: Working with Dates (Temporal.PlainDate)

The PlainDate type represents a calendar date without any time or timezone information. This is perfect for birthdays, holidays, deadlines—any date that doesn't have a specific time attached.

Creating PlainDate Objects

// From individual components (months are 1-indexed!)
const date1 = Temporal.PlainDate.from({ year: 2025, month: 1, day: 15 });
console.log(date1.toString()); // "2025-01-15"

// From a string (ISO 8601 format)
const date2 = Temporal.PlainDate.from('2025-01-15');

// Using the constructor
const date3 = new Temporal.PlainDate(2025, 1, 15);

// Get today's date
const today = Temporal.Now.plainDateISO();
console.log(today.toString()); // "2024-12-23" (today's date)
Enter fullscreen mode Exit fullscreen mode

Accessing Date Components

const date = Temporal.PlainDate.from('2025-06-15');

console.log(date.year);        // 2025
console.log(date.month);       // 6 (June, not 5!)
console.log(date.day);         // 15
console.log(date.dayOfWeek);   // 7 (Sunday, where Monday = 1)
console.log(date.dayOfYear);   // 166
console.log(date.weekOfYear);  // 24
console.log(date.daysInMonth); // 30
console.log(date.daysInYear);  // 365
console.log(date.inLeapYear);  // false
Enter fullscreen mode Exit fullscreen mode

Date Arithmetic (Finally, It Just Works)

const date = Temporal.PlainDate.from('2025-01-31');

// Add 1 month - Temporal handles the edge case!
const nextMonth = date.add({ months: 1 });
console.log(nextMonth.toString()); // "2025-02-28" - Clamped to valid date

// Add 2 weeks and 3 days
const later = date.add({ weeks: 2, days: 3 });
console.log(later.toString()); // "2025-02-17"

// Subtract time
const earlier = date.subtract({ months: 2, days: 10 });
console.log(earlier.toString()); // "2024-11-21"

// Complex operations chain beautifully
const result = date
  .add({ months: 6 })
  .add({ days: 15 })
  .subtract({ weeks: 1 });
console.log(result.toString()); // "2025-08-08"
Enter fullscreen mode Exit fullscreen mode

Comparing Dates

const date1 = Temporal.PlainDate.from('2025-01-15');
const date2 = Temporal.PlainDate.from('2025-03-20');

// Comparison methods
console.log(Temporal.PlainDate.compare(date1, date2)); // -1 (date1 is earlier)
console.log(date1.equals(date2)); // false

// Calculate difference
const diff = date2.since(date1);
console.log(diff.toString()); // "P64D" (64 days in ISO 8601 duration format)
console.log(diff.days); // 64

// Get difference in specific units
const diffInMonths = date2.since(date1, { largestUnit: 'month' });
console.log(diffInMonths.toString()); // "P2M5D" (2 months and 5 days)
Enter fullscreen mode Exit fullscreen mode

Part 2: Working with Times (Temporal.PlainTime)

PlainTime represents a wall-clock time without any date or timezone. Think of it as "what the clock shows": 10:30 AM, 14:45, etc.

Creating PlainTime Objects

// From components
const time1 = Temporal.PlainTime.from({ hour: 10, minute: 30 });
console.log(time1.toString()); // "10:30:00"

// With seconds and subseconds
const time2 = Temporal.PlainTime.from({
  hour: 14,
  minute: 30,
  second: 45,
  millisecond: 123,
  microsecond: 456,
  nanosecond: 789
});
console.log(time2.toString()); // "14:30:45.123456789"

// From string
const time3 = Temporal.PlainTime.from('10:30:00');

// Current time (without date)
const now = Temporal.Now.plainTimeISO();
Enter fullscreen mode Exit fullscreen mode

Time Arithmetic

const time = Temporal.PlainTime.from('10:30:00');

// Add 2 hours and 45 minutes
const later = time.add({ hours: 2, minutes: 45 });
console.log(later.toString()); // "13:15:00"

// What if we go past midnight?
const pastMidnight = time.add({ hours: 16 });
console.log(pastMidnight.toString()); // "02:30:00" - Wraps around!

// Subtract time
const earlier = time.subtract({ hours: 3 });
console.log(earlier.toString()); // "07:30:00"
Enter fullscreen mode Exit fullscreen mode

Rounding Time

const time = Temporal.PlainTime.from('10:37:42');

// Round to nearest 15 minutes
const rounded = time.round({ smallestUnit: 'minute', roundingIncrement: 15 });
console.log(rounded.toString()); // "10:45:00"

// Round to nearest hour
const hourly = time.round({ smallestUnit: 'hour' });
console.log(hourly.toString()); // "11:00:00"
Enter fullscreen mode Exit fullscreen mode

Part 3: Combining Date and Time (Temporal.PlainDateTime)

When you need both date and time but no specific timezone, use PlainDateTime. This is perfect for local events, appointments, or any time that's relative to the user's current location.

Creating PlainDateTime Objects

// From components
const dt1 = Temporal.PlainDateTime.from({
  year: 2025,
  month: 1,
  day: 15,
  hour: 10,
  minute: 30
});
console.log(dt1.toString()); // "2025-01-15T10:30:00"

// From string
const dt2 = Temporal.PlainDateTime.from('2025-01-15T10:30:00');

// Combine PlainDate and PlainTime
const date = Temporal.PlainDate.from('2025-01-15');
const time = Temporal.PlainTime.from('10:30:00');
const dt3 = date.toPlainDateTime(time);
console.log(dt3.toString()); // "2025-01-15T10:30:00"

// Current date and time
const now = Temporal.Now.plainDateTimeISO();
Enter fullscreen mode Exit fullscreen mode

Extracting Components

const dt = Temporal.PlainDateTime.from('2025-01-15T10:30:00');

// Get the date portion
const date = dt.toPlainDate();
console.log(date.toString()); // "2025-01-15"

// Get the time portion
const time = dt.toPlainTime();
console.log(time.toString()); // "10:30:00"

// Modify just the date
const newDt = dt.with({ month: 6, day: 20 });
console.log(newDt.toString()); // "2025-06-20T10:30:00"

// Modify just the time
const newDt2 = dt.with({ hour: 14 });
console.log(newDt2.toString()); // "2025-01-15T14:30:00"
Enter fullscreen mode Exit fullscreen mode

Part 4: The Real Power—Timezone Handling (Temporal.ZonedDateTime)

This is where Temporal truly shines. ZonedDateTime represents an exact moment in time, in a specific timezone, with full awareness of daylight saving time transitions and other timezone quirks.

Creating ZonedDateTime Objects

// From components with timezone
const zdt1 = Temporal.ZonedDateTime.from({
  year: 2025,
  month: 1,
  day: 15,
  hour: 10,
  minute: 30,
  timeZone: 'America/New_York'
});
console.log(zdt1.toString()); 
// "2025-01-15T10:30:00-05:00[America/New_York]"

// From ISO string with timezone
const zdt2 = Temporal.ZonedDateTime.from('2025-01-15T10:30:00[America/New_York]');

// From PlainDateTime + timezone
const dt = Temporal.PlainDateTime.from('2025-01-15T10:30:00');
const zdt3 = dt.toZonedDateTime('America/New_York');

// Current time in a specific timezone
const tokyoNow = Temporal.Now.zonedDateTimeISO('Asia/Tokyo');
console.log(tokyoNow.toString()); 
// "2024-12-23T22:30:00+09:00[Asia/Tokyo]" (example)
Enter fullscreen mode Exit fullscreen mode

Converting Between Timezones

const nyTime = Temporal.ZonedDateTime.from(
  '2025-01-15T10:30:00[America/New_York]'
);

// Convert to Tokyo time
const tokyoTime = nyTime.withTimeZone('Asia/Tokyo');
console.log(tokyoTime.toString()); 
// "2025-01-16T00:30:00+09:00[Asia/Tokyo]"

// Convert to UTC
const utc = nyTime.withTimeZone('UTC');
console.log(utc.toString()); 
// "2025-01-15T15:30:00+00:00[UTC]"

// Convert to London (accounts for its own offset)
const london = nyTime.withTimeZone('Europe/London');
console.log(london.toString()); 
// "2025-01-15T15:30:00+00:00[Europe/London]"
Enter fullscreen mode Exit fullscreen mode

Handling Daylight Saving Time

One of the trickiest aspects of timezone handling is DST transitions. Temporal handles these gracefully:

// In the US, clocks "spring forward" on the second Sunday of March
// 2:00 AM becomes 3:00 AM (2:30 AM doesn't exist!)

const beforeDST = Temporal.ZonedDateTime.from(
  '2025-03-09T01:30:00[America/New_York]'
);

// Add 1 hour - crosses the DST boundary
const afterDST = beforeDST.add({ hours: 1 });
console.log(afterDST.toString()); 
// "2025-03-09T03:30:00-04:00[America/New_York]"
// Notice: 1:30 AM + 1 hour = 3:30 AM (not 2:30 AM!)

// The offset changed from -05:00 to -04:00
console.log(beforeDST.offset); // "-05:00"
console.log(afterDST.offset);  // "-04:00"
Enter fullscreen mode Exit fullscreen mode

Handling Ambiguous and Invalid Times

During DST transitions, some times are invalid (don't exist) or ambiguous (occur twice):

// 2:30 AM on March 9th, 2025 doesn't exist in New York
// Temporal requires you to be explicit about how to handle this

const spring = Temporal.ZonedDateTime.from(
  '2025-03-09T02:30:00[America/New_York]',
  { disambiguation: 'compatible' } // Default: use the later offset
);
// This gets adjusted to 3:30 AM

// In fall, 1:30 AM happens TWICE (clocks fall back)
// Is it the first 1:30 AM or the second?

const fall = Temporal.ZonedDateTime.from(
  '2025-11-02T01:30:00[America/New_York]',
  { disambiguation: 'earlier' } // Use the first 1:30 AM (Daylight time)
);

const fallLater = Temporal.ZonedDateTime.from(
  '2025-11-02T01:30:00[America/New_York]',
  { disambiguation: 'later' } // Use the second 1:30 AM (Standard time)
);

console.log(fall.offset);      // "-04:00" (EDT)
console.log(fallLater.offset); // "-05:00" (EST)
Enter fullscreen mode Exit fullscreen mode

Part 5: Working with Durations (Temporal.Duration)

Duration represents a length of time, combining years, months, weeks, days, hours, minutes, seconds, and subseconds.

Creating Durations

// From components
const duration1 = Temporal.Duration.from({
  hours: 2,
  minutes: 30
});
console.log(duration1.toString()); // "PT2H30M"

// From ISO 8601 duration string
const duration2 = Temporal.Duration.from('P1Y2M3DT4H5M6S');
// 1 year, 2 months, 3 days, 4 hours, 5 minutes, 6 seconds

// Negative durations
const negative = Temporal.Duration.from({ hours: -3 });
console.log(negative.toString()); // "-PT3H"
Enter fullscreen mode Exit fullscreen mode

Duration Arithmetic

const d1 = Temporal.Duration.from({ hours: 1, minutes: 30 });
const d2 = Temporal.Duration.from({ hours: 2, minutes: 45 });

// Add durations
const sum = d1.add(d2);
console.log(sum.toString()); // "PT4H15M"

// Subtract durations
const diff = d2.subtract(d1);
console.log(diff.toString()); // "PT1H15M"

// Negate a duration
const negated = d1.negated();
console.log(negated.toString()); // "-PT1H30M"

// Get absolute value
const abs = negated.abs();
console.log(abs.toString()); // "PT1H30M"
Enter fullscreen mode Exit fullscreen mode

Balancing and Rounding Durations

Durations can be "unbalanced" (e.g., 90 minutes instead of 1 hour 30 minutes). Temporal lets you normalize them:

const unbalanced = Temporal.Duration.from({ minutes: 150 });
console.log(unbalanced.toString()); // "PT150M"

// Round to hours
const balanced = unbalanced.round({ largestUnit: 'hour' });
console.log(balanced.toString()); // "PT2H30M"

// Calculate total in a specific unit
const totalMinutes = unbalanced.total({ unit: 'minute' });
console.log(totalMinutes); // 150

const totalHours = unbalanced.total({ unit: 'hour' });
console.log(totalHours); // 2.5
Enter fullscreen mode Exit fullscreen mode

Practical Duration Examples

// Calculate age
const birthdate = Temporal.PlainDate.from('1990-05-15');
const today = Temporal.Now.plainDateISO();
const age = today.since(birthdate, { largestUnit: 'year' });
console.log(`Age: ${age.years} years, ${age.months} months, ${age.days} days`);

// Time until deadline
const deadline = Temporal.ZonedDateTime.from('2025-01-01T00:00:00[UTC]');
const now = Temporal.Now.zonedDateTimeISO('UTC');
const remaining = deadline.since(now, { largestUnit: 'day' });
console.log(`Countdown: ${remaining.days}d ${remaining.hours}h ${remaining.minutes}m`);

// Format a stopwatch
function formatStopwatch(ms: number): string {
  const duration = Temporal.Duration.from({ milliseconds: ms });
  const balanced = duration.round({ 
    largestUnit: 'minute',
    smallestUnit: 'second'
  });
  return `${String(balanced.minutes).padStart(2, '0')}:${String(balanced.seconds).padStart(2, '0')}`;
}
console.log(formatStopwatch(125000)); // "02:05"
Enter fullscreen mode Exit fullscreen mode

Part 6: Instants—Exact Moments in Time (Temporal.Instant)

While ZonedDateTime is timezone-aware, sometimes you just need to represent an exact moment in time without worrying about how it's displayed. That's where Instant comes in.

Instant is similar to the old Date in that it represents an absolute point on the timeline, but it's immutable and has a cleaner API.

Working with Instants

// Current instant
const now = Temporal.Now.instant();
console.log(now.toString()); // "2024-12-23T06:30:00.000000000Z"

// From epoch values
const fromMillis = Temporal.Instant.fromEpochMilliseconds(1703318400000);
const fromSeconds = Temporal.Instant.fromEpochSeconds(1703318400);

// From ISO string
const instant = Temporal.Instant.from('2025-01-15T10:30:00Z');

// Convert to epoch values
console.log(instant.epochMilliseconds); // 1736937000000
console.log(instant.epochSeconds);      // 1736937000
console.log(instant.epochNanoseconds);  // 1736937000000000000n (BigInt)
Enter fullscreen mode Exit fullscreen mode

Converting Between Instant and ZonedDateTime

const instant = Temporal.Instant.from('2025-01-15T15:30:00Z');

// View this instant in different timezones
const nyTime = instant.toZonedDateTimeISO('America/New_York');
console.log(nyTime.toString()); 
// "2025-01-15T10:30:00-05:00[America/New_York]"

const tokyoTime = instant.toZonedDateTimeISO('Asia/Tokyo');
console.log(tokyoTime.toString()); 
// "2025-01-16T00:30:00+09:00[Asia/Tokyo]"

// Go back from ZonedDateTime to Instant
const backToInstant = nyTime.toInstant();
console.log(backToInstant.equals(instant)); // true
Enter fullscreen mode Exit fullscreen mode

Part 7: Practical Real-World Examples

Let's look at some real-world scenarios where Temporal shines.

Example 1: Scheduling a Global Meeting

You're scheduling a meeting that needs to work for team members in New York, London, and Tokyo:

function scheduleMeeting(
  dateTime: string,
  hostTimezone: string,
  attendeeTimezones: string[]
): Map<string, string> {
  const meeting = Temporal.ZonedDateTime.from(
    `${dateTime}[${hostTimezone}]`
  );

  const schedule = new Map<string, string>();
  schedule.set(hostTimezone, meeting.toString());

  for (const tz of attendeeTimezones) {
    const local = meeting.withTimeZone(tz);
    schedule.set(tz, local.toString());
  }

  return schedule;
}

const times = scheduleMeeting(
  '2025-01-20T09:00:00',
  'America/New_York',
  ['Europe/London', 'Asia/Tokyo']
);

console.log(times);
// Map {
//   'America/New_York' => '2025-01-20T09:00:00-05:00[America/New_York]',
//   'Europe/London' => '2025-01-20T14:00:00+00:00[Europe/London]',
//   'Asia/Tokyo' => '2025-01-20T23:00:00+09:00[Asia/Tokyo]'
// }
Enter fullscreen mode Exit fullscreen mode

Example 2: Age Calculator with Precision

function calculateAge(birthdate: string): {
  years: number;
  months: number;
  days: number;
  totalDays: number;
  nextBirthday: Temporal.PlainDate;
  daysUntilBirthday: number;
} {
  const birth = Temporal.PlainDate.from(birthdate);
  const today = Temporal.Now.plainDateISO();

  // Calculate age
  const age = today.since(birth, { largestUnit: 'year' });

  // Total days lived
  const totalDays = today.since(birth).days;

  // Next birthday
  let nextBirthday = birth.with({ year: today.year });
  if (Temporal.PlainDate.compare(nextBirthday, today) <= 0) {
    nextBirthday = nextBirthday.add({ years: 1 });
  }
  const daysUntilBirthday = nextBirthday.since(today).days;

  return {
    years: age.years,
    months: age.months,
    days: age.days,
    totalDays,
    nextBirthday,
    daysUntilBirthday
  };
}

const result = calculateAge('1990-07-15');
console.log(result);
// {
//   years: 34,
//   months: 5,
//   days: 8,
//   totalDays: 12580,
//   nextBirthday: Temporal.PlainDate { ... },
//   daysUntilBirthday: 204
// }
Enter fullscreen mode Exit fullscreen mode

Example 3: Business Days Calculator

function addBusinessDays(
  start: Temporal.PlainDate,
  days: number
): Temporal.PlainDate {
  let current = start;
  let remaining = days;

  while (remaining > 0) {
    current = current.add({ days: 1 });
    // Skip weekends (6 = Saturday, 7 = Sunday)
    if (current.dayOfWeek < 6) {
      remaining--;
    }
  }

  return current;
}

const startDate = Temporal.PlainDate.from('2025-01-06'); // Monday
const endDate = addBusinessDays(startDate, 10);
console.log(endDate.toString()); // "2025-01-20" (skips 2 weekends)

// More sophisticated version with holidays
function addBusinessDaysWithHolidays(
  start: Temporal.PlainDate,
  days: number,
  holidays: Temporal.PlainDate[]
): Temporal.PlainDate {
  const holidayStrings = new Set(holidays.map(h => h.toString()));
  let current = start;
  let remaining = days;

  while (remaining > 0) {
    current = current.add({ days: 1 });
    const isWeekend = current.dayOfWeek >= 6;
    const isHoliday = holidayStrings.has(current.toString());

    if (!isWeekend && !isHoliday) {
      remaining--;
    }
  }

  return current;
}
Enter fullscreen mode Exit fullscreen mode

Example 4: Recurring Events (Monthly on the 15th)

function* generateMonthlyEvents(
  start: Temporal.PlainDate,
  dayOfMonth: number,
  count: number
): Generator<Temporal.PlainDate> {
  let current = start.with({ day: dayOfMonth });

  // If we're past this month's occurrence, start from next month
  if (Temporal.PlainDate.compare(current, start) < 0) {
    current = current.add({ months: 1 });
  }

  for (let i = 0; i < count; i++) {
    // Handle months with fewer days (e.g., February 30th doesn't exist)
    const daysInMonth = current.daysInMonth;
    const actualDay = Math.min(dayOfMonth, daysInMonth);
    yield current.with({ day: actualDay });

    current = current.add({ months: 1 });
  }
}

// Generate next 6 monthly events on the 31st
const events = Array.from(
  generateMonthlyEvents(
    Temporal.PlainDate.from('2025-01-01'),
    31,
    6
  )
);

console.log(events.map(e => e.toString()));
// ["2025-01-31", "2025-02-28", "2025-03-31", "2025-04-30", "2025-05-31", "2025-06-30"]
// Note: February uses 28th, April and June use 30th
Enter fullscreen mode Exit fullscreen mode

Example 5: Time Zone Safe Event Scheduler

interface Event {
  title: string;
  start: Temporal.ZonedDateTime;
  duration: Temporal.Duration;
}

class EventScheduler {
  private events: Event[] = [];

  addEvent(
    title: string,
    startTime: string,
    timezone: string,
    durationMinutes: number
  ): Event {
    const event: Event = {
      title,
      start: Temporal.ZonedDateTime.from(`${startTime}[${timezone}]`),
      duration: Temporal.Duration.from({ minutes: durationMinutes })
    };
    this.events.push(event);
    return event;
  }

  getEventsForDay(
    date: Temporal.PlainDate,
    timezone: string
  ): Event[] {
    return this.events.filter(event => {
      const eventInTz = event.start.withTimeZone(timezone);
      return eventInTz.toPlainDate().equals(date);
    });
  }

  getEventEndTime(event: Event): Temporal.ZonedDateTime {
    return event.start.add(event.duration);
  }

  hasConflict(newEvent: Event): boolean {
    const newEnd = this.getEventEndTime(newEvent);
    const newStartInstant = newEvent.start.toInstant();
    const newEndInstant = newEnd.toInstant();

    return this.events.some(existing => {
      const existingEnd = this.getEventEndTime(existing);
      const existingStartInstant = existing.start.toInstant();
      const existingEndInstant = existingEnd.toInstant();

      // Check for overlap
      return Temporal.Instant.compare(newStartInstant, existingEndInstant) < 0 &&
             Temporal.Instant.compare(newEndInstant, existingStartInstant) > 0;
    });
  }
}

// Usage
const scheduler = new EventScheduler();

scheduler.addEvent(
  'Team Standup',
  '2025-01-15T09:00:00',
  'America/New_York',
  30
);

scheduler.addEvent(
  'Client Call',
  '2025-01-15T14:00:00',
  'Europe/London',
  60
);

// Get all events for a specific day in Tokyo
const tokyoEvents = scheduler.getEventsForDay(
  Temporal.PlainDate.from('2025-01-15'),
  'Asia/Tokyo'
);
Enter fullscreen mode Exit fullscreen mode

Part 8: Migrating from Legacy Date Libraries

If you're currently using moment.js, date-fns, or luxon, here's how to migrate to Temporal.

From moment.js

// moment.js
const momentDate = moment('2025-01-15');
const momentFormatted = momentDate.format('YYYY-MM-DD');
const momentAdded = momentDate.add(1, 'month');
const momentDiff = moment('2025-03-15').diff(momentDate, 'days');

// Temporal equivalent
const temporalDate = Temporal.PlainDate.from('2025-01-15');
const temporalFormatted = temporalDate.toString(); // "2025-01-15"
const temporalAdded = temporalDate.add({ months: 1 });
const temporalDiff = Temporal.PlainDate.from('2025-03-15')
  .since(temporalDate)
  .days;

// moment.js timezone
const momentTz = moment.tz('2025-01-15 10:30', 'America/New_York');
const momentConverted = momentTz.tz('Asia/Tokyo');

// Temporal equivalent
const temporalTz = Temporal.ZonedDateTime.from(
  '2025-01-15T10:30:00[America/New_York]'
);
const temporalConverted = temporalTz.withTimeZone('Asia/Tokyo');
Enter fullscreen mode Exit fullscreen mode

From date-fns

// date-fns
import { addMonths, differenceInDays, format, parseISO } from 'date-fns';

const dateFnsDate = parseISO('2025-01-15');
const dateFnsAdded = addMonths(dateFnsDate, 1);
const dateFnsDiff = differenceInDays(
  parseISO('2025-03-15'),
  dateFnsDate
);
const dateFnsFormatted = format(dateFnsDate, 'yyyy-MM-dd');

// Temporal equivalent
const temporalDate = Temporal.PlainDate.from('2025-01-15');
const temporalAdded = temporalDate.add({ months: 1 });
const temporalDiff = Temporal.PlainDate.from('2025-03-15')
  .since(temporalDate)
  .days;
const temporalFormatted = temporalDate.toString();
Enter fullscreen mode Exit fullscreen mode

From luxon

// luxon
import { DateTime, Duration } from 'luxon';

const luxonDt = DateTime.fromISO('2025-01-15T10:30:00', {
  zone: 'America/New_York'
});
const luxonConverted = luxonDt.setZone('Asia/Tokyo');
const luxonDuration = Duration.fromObject({ hours: 2, minutes: 30 });

// Temporal equivalent
const temporalDt = Temporal.ZonedDateTime.from(
  '2025-01-15T10:30:00[America/New_York]'
);
const temporalConverted = temporalDt.withTimeZone('Asia/Tokyo');
const temporalDuration = Temporal.Duration.from({ hours: 2, minutes: 30 });
Enter fullscreen mode Exit fullscreen mode

Migration Strategy

  1. Start with new code: Use Temporal for all new features
  2. Create adapter functions: Bridge between legacy Date and Temporal
  3. Gradual replacement: Replace high-risk date code first
  4. Remove dependencies: Once migrated, remove moment.js/date-fns
// Adapter functions for gradual migration
function dateToTemporal(date: Date): Temporal.Instant {
  return Temporal.Instant.fromEpochMilliseconds(date.getTime());
}

function temporalToDate(instant: Temporal.Instant): Date {
  return new Date(instant.epochMilliseconds);
}

function dateToPlainDate(date: Date): Temporal.PlainDate {
  return Temporal.PlainDate.from({
    year: date.getFullYear(),
    month: date.getMonth() + 1, // Convert 0-indexed to 1-indexed!
    day: date.getDate()
  });
}
Enter fullscreen mode Exit fullscreen mode

Part 9: Browser Support and Polyfills

As of late 2024, Temporal is being actively implemented in all major browsers:

Browser Status
Chrome Behind flag (chrome://flags/#enable-javascript-harmony-temporal)
Firefox In development
Safari In development
Node.js Behind flag (--harmony-temporal)

Using Temporal Today with Polyfills

The official polyfill allows you to use Temporal now:

npm install @js-temporal/polyfill
Enter fullscreen mode Exit fullscreen mode
// Option 1: Import the polyfill
import { Temporal } from '@js-temporal/polyfill';

// Option 2: Global polyfill (adds to globalThis)
import '@js-temporal/polyfill';

// Now use Temporal normally
const today = Temporal.Now.plainDateISO();
Enter fullscreen mode Exit fullscreen mode

TypeScript Support

The polyfill includes TypeScript definitions:

// tsconfig.json
{
  "compilerOptions": {
    // ...
  }
}

// Your code
import { Temporal } from '@js-temporal/polyfill';

function addDays(
  date: Temporal.PlainDate,
  days: number
): Temporal.PlainDate {
  return date.add({ days });
}
Enter fullscreen mode Exit fullscreen mode

Bundle Size Considerations

The Temporal polyfill is substantial (~40KB minified + gzipped) because it includes the full IANA timezone database. Consider:

  1. Code splitting: Only load the polyfill when needed
  2. Feature detection: Check if native Temporal is available
// Feature detection
const hasNativeTemporal = typeof globalThis.Temporal !== 'undefined';

// Dynamic import for polyfill
async function getTemporal() {
  if (hasNativeTemporal) {
    return globalThis.Temporal;
  }
  const { Temporal } = await import('@js-temporal/polyfill');
  return Temporal;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion: The Future of Date Handling in JavaScript

The Temporal API represents the most significant improvement to JavaScript's date handling in the language's history. By the end of 2025, it will be universally available in all modern environments.

Here's what makes Temporal transformative:

  1. Immutability eliminates hidden bugs: No more accidentally modified dates
  2. Explicit types clarify intent: PlainDate vs ZonedDateTime vs Instant
  3. Human-friendly API: 1-indexed months, clear method names
  4. First-class timezone support: Built-in IANA database, DST handling
  5. Powerful duration arithmetic: Finally, date math that just works

Action Items

  1. Start experimenting today using the polyfill
  2. Use Temporal for new projects when targeting modern browsers
  3. Plan your migration from moment.js/date-fns
  4. Follow browser implementation progress for production deployment

The days of battling JavaScript's broken Date object are numbered. Temporal is here, and it's everything we've been waiting for.


Want to test your date/time code? Try our collection of developer tools at Pockit.tools for JSON formatting, encoding/decoding, and more utilities that make development easier.


💡 Note: This article was originally published on the Pockit Blog.

Check out Pockit.tools for 50+ free developer utilities (JSON Formatter, Diff Checker, etc.) that run 100% locally in your browser.

Top comments (0)