In JavaScript's Temporal API, what do Temporal.Instant, Temporal.PlainDateTime and Temporal.ZonedDateTime each model, and what extra information does converting between them require?
answer
- three levels of specificity
- exact time versus wall clock
- one of them names no moment
- widening needs an argument
- zone id, not just an offset
basics
~20 sTemporal.Instant is a fixed point on the global timeline with no zone or calendar. Temporal.PlainDateTime is a wall-clock reading with no zone, so it names no exact time. Temporal.ZonedDateTime is both together, carrying an IANA time-zone ID.
solid answer
~40 sTemporal splits what the old `Date` type conflated. A `Temporal.Instant` is an exact point on the global timeline, stored as nanoseconds since the epoch — it has no calendar, no zone, and no notion of "what day it is". A `Temporal.PlainDateTime` is the opposite: a calendar date plus a wall-clock time, like `2026-06-01T19:00`, with no zone or offset, so it does not identify a moment at all. A `Temporal.ZonedDateTime` is the full value — an exact instant *plus* an IANA time-zone ID such as `Europe/Berlin` and a calendar ID — and it prints as `2026-06-01T19:00:00+02:00[Europe/Berlin]`. Converting downward always loses information and is free: `zdt.toInstant()`, `zdt.toPlainDateTime()`. Converting upward needs the missing piece supplied explicitly: `instant.toZonedDateTimeISO(timeZone)` or `plainDateTime.toZonedDateTime(timeZone)`. That forced explicitness is the whole design point.
code
javascript · 14 linesimport { Temporal } from '@js-temporal/polyfill';
const instant = Temporal.Instant.from('2026-06-01T17:00:00Z');
// Up: must supply the missing time zone
const zdt = instant.toZonedDateTimeISO('Europe/Berlin');
console.log(zdt.toString()); // 2026-06-01T19:00:00+02:00[Europe/Berlin]
// Down: lossy but unambiguous, no arguments needed
const pdt = zdt.toPlainDateTime();
console.log(pdt.toString()); // 2026-06-01T19:00:00
// Round trip only works because the zone is supplied again
console.log(pdt.toZonedDateTime('Europe/Berlin').toInstant().equals(instant)); // truego deeper
Be able to name the three and say which one has no time zone. Say plainly that a wall-clock reading like 2026-06-01T19:00 does not identify a moment until someone names a place.
Explain the conversion graph in both directions and why widening takes an argument while narrowing does not. Know that Instant is epoch nanoseconds with no calendar, and that ZonedDateTime carries an IANA ID rather than a bare offset.
Show that you pick types by what the data actually asserts — exact time for things that happened, wall clock plus zone for things people will read off a clock — and explain how that choice removes a class of zone bugs that testing on one machine never catches.
Own the argument that Temporal's value is a type discipline, not new formatting: making the missing zone a compile-time-shaped absence rather than an implicit default is what changes team-wide defect rates. Be ready to say where the discipline costs you and how you enforce it at storage and API boundaries.
## The conflation Temporal is undoing A single legacy `Date` value is simultaneously an epoch timestamp and, through its local getters, a wall-clock reading in whatever zone the machine happens to be configured for. Those are two different kinds of fact, and code that mixes them produces bugs that only appear on machines in other zones. Temporal's central design move is to give each kind of fact its own immutable type, and to make you say out loud what is missing whenever you move between them. (Temporal is a Stage 3 TC39 proposal. As of 2026 Firefox ships it and other engines are in progress; most production code still reaches it through the official `@js-temporal/polyfill`.) ## Temporal.Instant — the exact time An `Instant` is a point on the global timeline: nanoseconds since the Unix epoch, exposed as `instant.epochNanoseconds` (a BigInt) and `instant.epochMilliseconds` (a Number). It has no calendar and no time zone, so it has no `.year`, no `.hour`, and no `.dayOfWeek` — asking "what date is this Instant?" is a category error until you name a zone. Two Instants can be ordered without any further context (`Temporal.Instant.compare(a, b)`), because exact times are globally comparable. Its `toString()` produces a `Z`-suffixed ISO string such as `2026-06-01T17:00:00Z`. Because it has no calendar, `Instant` arithmetic accepts only exact-time units — hours, minutes, seconds and smaller. You cannot add a month to an Instant, because "a month" has no length without a calendar. ## Temporal.PlainDateTime — the wall-clock reading A `PlainDateTime` is a date and a time of day with a calendar but *no zone and no offset*: `2026-06-01T19:00`. It is what a wall clock or a paper form says. Crucially it does not identify a moment — `2026-06-01T19:00` happened at a different instant in Berlin than in São Paulo, and on a DST transition day a given wall-clock reading may not exist at all, or may exist twice. The same family includes narrower plain types for the cases where you genuinely have less information: `Temporal.PlainDate` (a calendar day, e.g. a date of birth), `Temporal.PlainTime` (a time of day, e.g. a shop's opening hour), `Temporal.PlainYearMonth` (a card expiry) and `Temporal.PlainMonthDay` (a recurring anniversary). Choosing the narrowest type that fits is itself a correctness tool: a `PlainDate` cannot accidentally acquire a spurious midnight-in-UTC. ## Temporal.ZonedDateTime — both at once A `ZonedDateTime` carries an exact time, an IANA time-zone ID and a calendar ID together. It therefore answers both questions: `zdt.epochNanoseconds` gives the exact time, `zdt.hour` gives the wall-clock reading, `zdt.timeZoneId` gives `"Europe/Berlin"`, `zdt.offset` gives `"+02:00"`, and `zdt.calendarId` gives `"iso8601"`. Its string form round-trips all of it: ```js Temporal.ZonedDateTime.from('2026-06-01T19:00[Europe/Berlin]').toString(); // '2026-06-01T19:00:00+02:00[Europe/Berlin]' ``` The zone ID matters more than the offset. An offset like `+02:00` is a snapshot of one moment's rules; the ID is the rule set itself, so arithmetic across a DST boundary still lands on the right wall-clock time. ## The conversion graph Going from more information to less is lossy but unambiguous, so it needs no arguments: ```js const zdt = Temporal.ZonedDateTime.from('2026-06-01T19:00[Europe/Berlin]'); zdt.toInstant(); // drops zone + calendar zdt.toPlainDateTime(); // drops zone, keeps wall clock zdt.toPlainDate(); // just the calendar day ``` Going the other way requires you to supply what is missing, which is where Temporal refuses to guess: ```js const instant = Temporal.Instant.from('2026-06-01T17:00:00Z'); instant.toZonedDateTimeISO('Europe/Berlin'); // 2026-06-01T19:00:00+02:00[Europe/Berlin] const pdt = Temporal.PlainDateTime.from('2026-06-01T19:00'); pdt.toZonedDateTime('Europe/Berlin').toInstant().equals(instant); // true ``` There is no `PlainDateTime.prototype.toInstant()` at all — the type system makes the missing zone impossible to forget. ## Picking the right one A useful rule: if the fact is "something happened", it is an exact time and belongs in an `Instant` — log lines, audit records, payment captures. If the fact is "a human will read this on a clock", it is a wall-clock value and belongs in a `PlainDateTime` (or a narrower plain type) together with the zone it belongs to. `ZonedDateTime` is what you compute with when you need both views at once — anything doing calendar arithmetic in a real place, such as "the same time tomorrow" or "the start of the business day". Current time is obtained per shape: `Temporal.Now.instant()`, `Temporal.Now.zonedDateTimeISO()`, `Temporal.Now.plainDateISO()` — again, you choose the precision of the truth you want rather than getting one type that pretends to be all three.
- Why does Temporal keep an IANA zone ID on ZonedDateTime instead of just storing the UTC offset?An offset such as `+02:00` describes one moment; a zone ID such as `Europe/Berlin` is the rule set that generates offsets. With the ID, adding a day across a DST boundary still lands on the same wall-clock time and the correct new offset. With only an offset you can order instants but cannot do correct local arithmetic, and you cannot tell `+02:00` in Berlin from `+02:00` in Johannesburg.
- When would you reach for Temporal.PlainDate or Temporal.PlainYearMonth rather than PlainDateTime?When the extra precision would be fiction. A date of birth is a `PlainDate`: attaching midnight to it invites a zone conversion that shifts it a day. A card expiry is a `PlainYearMonth`. Picking the narrowest type means the impossible conversion is a missing method rather than a silent wrong answer.
- Why is there no PlainDateTime.prototype.toInstant()?Because the conversion is genuinely undecidable without a zone, and on DST transition days it can be undecidable even with one — the wall-clock reading may not exist or may exist twice. Temporal forces you through `toZonedDateTime(timeZone)`, where you can also pass a `disambiguation` option for those cases.
saying these in an interview costs you the question
- Instant and ZonedDateTime are the same value with different formatting
- A PlainDateTime can be converted to UTC on its own
- Storing a fixed UTC offset is as good as storing the zone ID
- Temporal.Instant has a year and month you can read
- ZonedDateTime is always the safest type, so use it everywhere