Temporal API for TS/JS dates and times. Use when parsing, formatting or comparing dates, computing today, time zones, durations or working days.
Scanned 9/29/2026
npx -y skills add laurigates/claude-plugins --skill typescript-temporal --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Typescript Temporal?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/laurigates-typescript-temporal)More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.
---
created: 2026-09-29
modified: 2026-09-29
reviewed: 2026-09-29
name: typescript-temporal
description: "Temporal API for TS/JS dates and times. Use when parsing, formatting or comparing dates, computing today, time zones, durations or working days."
user-invocable: false
allowed-tools: Glob, Grep, Read, Bash, Edit, Write, TodoWrite, WebFetch, WebSearch
---
# 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
```ts
// 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 with `ReferenceError: Temporal is not defined`. Existing
users on older Chrome stop receiving updates.
- **TypeScript**: types ship from TypeScript 6.0. Add `"ESNext"` to `lib`, or
the granular `"esnext.temporal"` plus `"esnext.date"` (for
`toTemporalInstant`). TypeScript 5.x has no Temporal types; with the polyfill,
add `import "temporal-polyfill/types/global"`. The rest of the tsconfig
belongs to `typescript-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:
```bash
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-polyfill
- `minimum_chrome_version`: https://developer.chrome.com/docs/extensions/reference/manifest/minimum-chrome-version
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!