Migrating from raw TC39 Temporal
If you are already using the raw TC39 Temporal API, atemporal gives you a much friendlier ergonomic layer on top — without giving up access to the underlying Temporal.* values when you need them.
The mental model
| Raw Temporal | atemporal |
|---|---|
Temporal.PlainDate.from(...) | atemporal(...) (one entry point) |
Temporal.PlainDateTime.from(...) | atemporal(...) |
Temporal.ZonedDateTime.from(...) | atemporal(..., tz) |
Temporal.Instant.from(...) | atemporal(...).instant() (escape hatch) |
dt.add({ days: 1 }) | dt.add(1, 'day') |
dt.subtract({ months: 3 }) | dt.subtract(3, 'month') |
Temporal.PlainDate.compare(a, b) | a.until(b).total('day') |
dt.toString() | dt.format(atemporal.presets.ISO) |
dt.toLocaleString(locale, opts) | dt.format(opts, locale) |
Why use atemporal on top of Temporal?
One entry point. atemporal's
atemporal(input, tz)accepts strings, numbers,Date,Temporal.*values, plain objects, Firebase Timestamps, etc. With raw Temporal you have to pick the rightfrommethod for each input type.Immutability by default. atemporal's
TemporalWrapperis always immutable. Raw Temporal also returns new values, but you can accidentally pass a mutable array/object literal in some operations.Fluent chained API.
atemporal().add(1, 'day').startOf('week').format('YYYY-MM-DD')reads top-to-bottom. Raw Temporal requires nested calls.Format strings. atemporal's formatting engine takes moment-style
YYYY-MM-DDtokens, which your team already knows. Raw Temporal requiresIntl.DateTimeFormatoptions.Sensible defaults. atemporal handles "now", default timezone, and default locale with three setters; raw Temporal makes you pass
Temporal.Now.*()and an explicit timezone to every call.Error codes. atemporal's errors carry stable
ATEMPORAL_*codes for i18n, structured logging, and dashboards.Strict mode.
atemporal.setStrictMode(true)warns on ambiguous operations in development.
The escape hatches
atemporal is a layer, not a replacement. You can drop down to raw Temporal at any time:
const a = atemporal('2024-01-15');
// Get the underlying ZonedDateTime:
a.toZonedDateTime(); // or a['_datetime'] (private)
// Construct from raw Temporal:
atemporal(Temporal.PlainDate.from('2024-01-15'));
// Wrap a Temporal.Duration:
atemporal.duration({ days: 1 });
// Native runtime: use the native global only after confirming it is available.
const zdt = globalThis.Temporal.ZonedDateTime.from('2024-01-15T00:00:00[UTC]');
const wrapped = atemporal(zdt);Do not assume globalThis.Temporal exists. When getTemporalInfo().isNative is false and your application needs raw Temporal values, declare @js-temporal/polyfill in the application and import Temporal from it:
import { Temporal } from "@js-temporal/polyfill";
const zdt = Temporal.ZonedDateTime.from('2024-01-15T00:00:00[UTC]');
const wrapped = atemporal(zdt);The polyfill is already an atemporal runtime dependency, but an application that imports it directly should declare it explicitly rather than relying on transitive dependency layout.
Performance: native vs polyfill
atemporal automatically uses the native Temporal implementation when the runtime exposes the required Temporal surface and falls back to @js-temporal/polyfill otherwise. Use getTemporalInfo() in the actual deployment rather than assuming native availability from a browser or Node version. CI validates the native path on Node 26.
The polyfill is statically imported as a direct runtime dependency. Native selection changes the implementation used at runtime; it does not, by itself, guarantee that a bundler removes the polyfill from an application bundle.
You can check which one is in use:
atemporal.getTemporalInfo();
// { isNative: true, environment: 'node', version: 'native' }
// or
// { isNative: false, environment: 'browser', version: 'polyfill' }Patterns atemporal adds on top
Validation without throwing
// Raw Temporal — throws RangeError
try { Temporal.PlainDate.from('not a date'); } catch (e) { ... }
// atemporal — structured result, no throw
const r = atemporal.validate('not a date');
if (!r.ok) {
console.error(r.code, r.reason);
}Try-style construction
// atemporal — returns null on failure
const a = atemporal.try(input);
if (a === null) { /* skip */ }Preset format strings
// atemporal — no string juggling
m.format(atemporal.presets.RFC2822);
m.format(atemporal.presets.SQL_DATETIME);
m.format(atemporal.presets.ISO);Strict mode for safety in production
// atemporal — opt-in during test/CI runs
atemporal.setStrictMode(true);What atemporal does not hide
- The fact that
Temporal.PlainDatehas no time component. - The fact that calendar arithmetic can produce out-of-range values (
month: 13→ ISO month 1 of next year). - The fact that zoned datetimes have non-trivial IANA semantics (DST, gap, overlap).
These are features of the underlying spec, and atemporal faithfully exposes them.