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!
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);
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
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
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
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:
- Immutability: All Temporal objects are immutable. Operations return new objects.
- Explicit Types: Different types for different use cases (dates, times, timezones, durations).
- No Surprises: Months are 1-indexed. Parsing is strict. Behavior is consistent.
- Timezone-Aware by Design: Built-in support for IANA timezone database.
- 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)
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)
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
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"
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)
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();
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"
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"
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();
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"
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)
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]"
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"
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)
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"
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"
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
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"
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)
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
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]'
// }
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
// }
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;
}
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
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'
);
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');
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();
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 });
Migration Strategy
- Start with new code: Use Temporal for all new features
- Create adapter functions: Bridge between legacy Date and Temporal
- Gradual replacement: Replace high-risk date code first
- 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()
});
}
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
// 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();
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 });
}
Bundle Size Considerations
The Temporal polyfill is substantial (~40KB minified + gzipped) because it includes the full IANA timezone database. Consider:
- Code splitting: Only load the polyfill when needed
- 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;
}
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:
- Immutability eliminates hidden bugs: No more accidentally modified dates
- Explicit types clarify intent: PlainDate vs ZonedDateTime vs Instant
- Human-friendly API: 1-indexed months, clear method names
- First-class timezone support: Built-in IANA database, DST handling
- Powerful duration arithmetic: Finally, date math that just works
Action Items
- Start experimenting today using the polyfill
- Use Temporal for new projects when targeting modern browsers
- Plan your migration from moment.js/date-fns
- 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)