Temporal is the TC39 proposal that replaces the flawed Date object with a family of immutable,
purpose-built date and time types. This reference lists each type, what it models, and its key
methods.
Why Temporal replaces Date
The legacy Date object has been broken by design since 1995. Its most painful problems:
- Mutability.
date.setMonth(1)modifies in place — pass aDateto a function and the caller’s copy can change. - Month indexing. January is
0, December is11. Off-by-one bugs are silent and common. - One type for everything.
Datetries to model both “a calendar date” and “a UTC timestamp” using the same object, exposing confusing local-vs-UTC accessor pairs (getMonthvsgetUTCMonth). - No time-zone support. You only get the local system zone or UTC; named time zones like
"America/New_York"are not part of the API. - Inconsistent parsing.
new Date("2026-06-06")returns midnight UTC on most engines, but results vary.
How it works
Instead of one overloaded Date, Temporal gives you a type per concept:
Temporal.Now.plainDateISO(); // today's date, no time/zone
Temporal.PlainDate.from("2026-06-06"); // a calendar date
Temporal.ZonedDateTime.from("2026-06-06T09:00[Europe/London]");
Temporal.Duration.from({ hours: 2, minutes: 30 });
The core split is wall-clock vs exact time:
PlainDate,PlainTime,PlainDateTime,PlainYearMonth,PlainMonthDaycarry no time zone — they are “what a clock or calendar on the wall reads”.Instantis an exact point on the global timeline (a UTC timestamp), with no local interpretation.ZonedDateTimeties an instant to a time zone + calendar, so it knows local time, offset and DST rules.
Duration represents a length of time and is what until, since, add and subtract produce or
consume. Every type is immutable — add/with/round return new objects.
Choosing the right type
| Situation | Use |
|---|---|
| A birthday, invoice date, or holiday | PlainDate |
| A meeting time with no explicit time zone | PlainDateTime |
| A scheduled alarm in a specific city | ZonedDateTime |
| A server log timestamp or database record | Instant |
| Adding or measuring elapsed time | Duration |
Practical worked examples
Date arithmetic across a DST boundary
One of the most confusing things in legacy Date is adding days across a daylight-saving clock change. In Temporal the distinction is explicit:
const startNY = Temporal.ZonedDateTime.from("2026-03-07T10:00[America/New_York]");
// "Add 1 day" = same wall-clock time tomorrow (may be 23 hours of real time):
const nextDay = startNY.add({ days: 1 });
// → 2026-03-08T10:00:00-04:00[America/New_York]
// "Add 24 hours" = exactly 24 hours of real elapsed time:
const plus24h = startNY.add({ hours: 24 });
// → 2026-03-08T11:00:00-04:00[America/New_York] (one hour later because clocks sprang forward)
For example, a reminder app that says “notify me at 10 AM tomorrow” should use { days: 1 }. A countdown timer that says “notify me in 24 hours” should use { hours: 24 }.
Calculating age in years
const birthday = Temporal.PlainDate.from("1990-09-15");
const today = Temporal.Now.plainDateISO();
const age = birthday.until(today, { largestUnit: "years" });
// age.years → the person's current age in whole years
This respects leap years correctly and never has the off-by-one month error that plagues Date-based age calculations.
Tips and notes
- Months are 1-indexed in Temporal (January is 1), fixing the classic
Datefootgun wherenew Date(2026, 0, 1)is January 1. - Adding
{ days: 1 }to aZonedDateTimekeeps the same wall time across DST (may be 23 or 25 hours); adding{ hours: 24 }always adds 24 real hours. - Use
compare(a, b)static methods to sort; the==/<operators do not work on objects. - Convert between types with methods like
.toZonedDateTime(tz),.toInstant(),.toPlainDate(). - The
@js-temporal/polyfillpackage brings Temporal to environments that do not have it natively yet.