Typescript Temporal
Temporal API for TS/JS dates and times.
by laurigates · MIT
The skill
Temporal API for TS/JS dates and times. Use when parsing, formatting or comparing dates, computing today, time zones, durations or working days.
Temporal for Dates and Times
New TypeScript and JavaScript code handles dates and times with Temporal.
Date packs a calendar date, an instant and the host time zone into one
mutable object. The bugs that follow all type-check: yesterday's date just
after midnight, every date shifted a day west of UTC, weekday indexes off by
one. Temporal gives each meaning its own type, so mixing them becomes a compile
error.
Pick the type by what the value means
| The value is… | Type | Example |
|---|---|---|
| A calendar date with no time or zone (due date, holiday, timesheet day) | Temporal.PlainDate |
Temporal.PlainDate.from("2026-06-15") |
| A month (billing or reporting period) | Temporal.PlainYearMonth |
Temporal.PlainYearMonth.from("2026-02").daysInMonth is 28 |
| A wall-clock time, or date and time, with no zone ("09:00 every weekday") | Temporal.PlainTime / Temporal.PlainDateTime |
Temporal.PlainTime.from("09:00") |
A moment in time (log timestamp, created_at) |
Temporal.Instant |
Temporal.Now.instant() |
| A moment shown or bucketed in a place | Temporal.ZonedDateTime with an explicit IANA zone |
instant.toZonedDateTimeISO("Europe/Helsinki") |
| A length of time | Temporal.Duration |
Temporal.Duration.from({ hours: 7, minutes: 15 }).total("hours") is 7.25 |
If answering "which day is this?" needs a time zone, the value is not a
PlainDate yet.
Convert a timestamp to a date once, at the boundary, in a named zone
// Wrong: the UTC calendar date, not the user's. In Finland (UTC+2/+3) this is
// yesterday from 00:00 to 02:00 local in winter and to 03:00 in summer.
const utcToday = new Date().toISOString().slice(0, 10); // or .split("T")[0]
// Right: one "today" seam for the whole codebase (see Testing for why it
// reads Date.now())
const TZ = "Europe/Helsinki";
export const today = (tz = TZ): Temporal.PlainDate =>
Temporal.Instant.fromEpochMilliseconds(Date.now()).toZonedDateTimeISO(tz).toPlainDate();
const day = Temporal.Instant.from(apiTimestamp).toZonedDateTimeISO(TZ).toPlainDate();
Pass the zone explicitly. With no argument, Temporal.Now.plainDateISO() uses
the host zone, which on servers and CI runners is usually UTC. An unknown zone
ID throws RangeError.
Keep calendar dates out of Date
The "UTC-midnight Date" pattern (new Date("2026-06-15") read back with
getUTC*) stays correct only while every caller remembers the UTC getters. A
single getDate(), getDay() or toLocaleDateString() reads it in local time,
and west of UTC the date moves back a day: with TZ=America/New_York,
new Date("2026-06-15").getDate() is 14. Both getters return number, so
the type checker cannot see the mistake. A PlainDate has no time or zone to
misread.
Store and transmit ISO strings
Keep ISO 8601 strings ("2026-06-15", "2026-06-15T09:00:00Z") in JSON,
databases, chrome.storage and URLs. Parse at the edge with
Temporal.<Type>.from(str), emit with .toString(); JSON.stringify produces
the same string through toJSON. String input with an impossible date throws
(PlainDate.from("2026-02-30") is a RangeError), but property-bag input
clamps by default ({ year: 2026, month: 2, day: 30 } becomes 2026-02-28),
so pass { overflow: "reject" } when validating user input.
Interop with legacy Date and libraries happens at that same boundary:
| Direction | Call |
|---|---|
Date to Instant |
date.toTemporalInstant() |
Instant to Date |
new Date(instant.epochMilliseconds) |
Epoch ms to Instant |
Temporal.Instant.fromEpochMilliseconds(ms) |
Porting from Date
Date habit |
Temporal |
|---|---|
getDay(): 0 = Sunday … 6 = Saturday |
dayOfWeek: 1 = Monday … 7 = Sunday; weekend is d.dayOfWeek >= 6 |
getMonth() is 0-based |
month is 1-based |
a < b, a - b |
Temporal.PlainDate.compare(a, b), a.until(b); <, - and "" + d throw TypeError because valueOf throws |
a.getTime() === b.getTime() |
a.equals(b); === compares object identity |
d.setDate(d.getDate() + 1) mutates |
d.add({ days: 1 }) returns a new value |
| Jan 31 + 1 month rolls into March | PlainDate.from("2026-01-31").add({ months: 1 }) is 2026-02-28 |
Template literals call toString() and are safe: `${date}`.
Testing
A pinned test clock reaches Temporal.Now only if the fake-timer library fakes
it. @sinonjs/fake-timers added Temporal.Now.* faking in 15.4.0
(2026-05-05). Measured on Node 26 after pinning the clock to 2020-01-15:
| Runner | Temporal.Now.instant() |
|---|---|
Vitest 4.1.4, 4.1.6, 4.1.11, vi.useFakeTimers() + vi.setSystemTime() |
real time; only Date is pinned |
Vitest 5.0.0 and 5.0.2 (5.0.0 release notes: "support mocking Temporal") |
pinned |
Jest 30.5.2, jest.useFakeTimers({ now }) |
pinned; @jest/fake-timers 30.4.0+ requires fake-timers ^15.4.0 |
Bun 1.4.2, setSystemTime() |
pinned |
An explicit toFake list that leaves out "Temporal" also leaves it real. On
runners that do not fake it, code that calls Temporal.Now.* directly ignores
the pinned clock and nothing fails loudly. The today() seam above reads
Date.now(), which every fake-timer version pins, so it behaves the same on
every runner; injecting a clock (now: () => Temporal.Instant) works as well.
bun test runs in UTC unless TZ is set. In UTC, UTC slicing and local
getters agree, so both off-by-one-day bugs above pass. Run date tests under a
zone east and a zone west of UTC.
Availability
Native support per MDN browser-compat-data (8.1.3, 2026-09-24):
| Runtime | Native from |
|---|---|
| Chrome, Edge | 144 |
| Firefox | 139 |
| Safari | Technology Preview only; not in stable Safari or iOS |
| Node.js | 26.0.0 (earlier majors: polyfill) |
| Deno | 2.7 |
| Bun | 1.4.0 |
- Older targets:
temporal-polyfill(FullCalendar, ~20 kB, defers to native Temporal when present). Import"temporal-polyfill/global"once at the entry point. MDN also lists@js-temporal/polyfill; its latest release (0.5.1, March 2025) predates Stage 4. - Chrome extensions: set
"minimum_chrome_version": "144"in the manifest. The Web Store then shows older Chrome "Not compatible" instead of the extension failing withReferenceError: Temporal is not defined. Existing users on older Chrome stop receiving updates. - TypeScript: types ship from TypeScript 6.0. Add
"ESNext"tolib, or the granular"esnext.temporal"plus"esnext.date"(fortoTemporalInstant). TypeScript 5.x has no Temporal types; with the polyfill, addimport "temporal-polyfill/types/global". The rest of the tsconfig belongs totypescript-strict.
When Date still appears
New code that reaches for new Date(, Date.now(), moment, date-fns, dayjs or
luxon uses Temporal instead. The exception is an external API that takes or
returns Date (a library signature, valueAsDate, a database driver): convert
at that call site and keep Temporal on both sides. The today() seam's
Date.now() is the other exception. Migrate existing Date code
when it is touched for a date bug, not as a drive-by sweep.
Agentic Optimizations
| Context | Command |
|---|---|
| Runtime has native Temporal | node -p 'typeof Temporal' (object) |
| Types resolve | bunx tsc --noEmit |
| Reproduce a zone bug | TZ=America/New_York bun test |
Find the patterns this skill replaces:
rg -n "toISOString\(\)\.(slice\(0, ?10\)|split\(['\"]T['\"]\))" src
rg -n 'new Date\(|Date\.now\(\)|from "(moment|date-fns|dayjs|luxon)' src
rg -n '\.get(Day|Date|Month|FullYear)\(\)' src
References
- MDN Temporal: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal
- Proposal docs and cookbook: https://tc39.es/proposal-temporal/docs/
temporal-polyfill: https://github.com/fullcalendar/temporal-polyfillminimum_chrome_version: https://developer.chrome.com/docs/extensions/reference/manifest/minimum-chrome-version
Use this skill
Connect your agent to the Skill Store MCP server and ask for it by ref: laurigates/typescript-temporal-22765412048.
Details
- Publisher
- laurigates
- Category
- Software Development › Languages & frameworks
- License
- MIT
- Updated
- 2026-10-19
References
More from laurigates
- Docs Refresh Refresh plugin catalog docs (README, PLUGIN-MAP, d2 diagram) so per-plugin skill/agent counts mat…
- Plugin Authoring Add, modify, or delete a skill or plugin in this repo — frontmatter shape, the seven metadata fil…
- Accessibility Implementation WCAG 2.1/2.2 compliance, ARIA patterns, keyboard nav, focus management, a11y testing.
- Design Tokens Design tokens and CSS custom properties: theme systems, dark mode, component libraries.