DateTime

🕒 An immutable date and time in an IANA time zone.

Info

Backed by the TC39 Temporal API where the runtime provides it, and by Intl.DateTimeFormat everywhere else. Both paths implement the same semantics, so behaviour does not change with the host. Time-zone support needs no bundled database and no dependency.

Months and days are 1-based and dayOfWeek runs 1 (Monday) to 7 (Sunday), following Temporal and ISO-8601 rather than the legacy Date. Every method returns a new instance; nothing mutates.

Syntax

TypeScript
import { DateTime } from '@opentf/std';
new DateTime(input?, options?);

Parameters

NameTypeDescription
inputstring | number | Date | DateTime | DateTimeFieldsISO-8601 string, epoch milliseconds, Date, DateTime or fields. Omit for now.
options{ timeZone?: string, disambiguation?: Disambiguation }timeZone defaults to 'UTC'; disambiguation to 'compatible'.

Parsing is strict ISO-8601 — it throws a RangeError rather than yielding an Invalid Date, because Date.parse is implementation-defined outside the ISO subset and disagrees across runtimes.

TypeScript
new DateTime('2026-07-29');
new DateTime('2026-07-29T14:30:15.250');
new DateTime('2026-07-29T14:30:15+05:30');
new DateTime('2026-07-29T14:30:15+05:30[Asia/Kolkata]');
new DateTime(1785315615250);
new DateTime({ year: 2026, month: 7, day: 29 }, { timeZone: 'Asia/Kolkata' });

Statics

MemberReturnsDescription
DateTime.now(tz?)DateTimeThe current time.
DateTime.compare(a, b)-1 | 0 | 1Comparator for Array.prototype.sort.
DateTime.hasTemporalbooleanWhether this runtime provides Temporal.
DateTime.fromTemporal(v)DateTimeFrom a Temporal.ZonedDateTime or Temporal.Instant.

Getters

MemberDescriptionMemberDescription
year month day1-based month and daydayOfWeek1 = Monday … 7 = Sunday
hour minute secondwall clockdayOfYear1–366
millisecond0–999weekOfYear weekYearISO-8601 week numbering
timeZoneIANA iddaysInMonth daysInYearcalendar lengths
offset offsetMs'+05:30' / 19800000inLeapYearboolean
epochMsepoch milliseconds

Methods

MemberReturnsDescription
add(duration)DateTimeAdvances by calendar and/or exact units.
subtract(duration)DateTimeAs add, with every value negated.
with(fields)DateTimeReplaces wall-clock fields, keeping the zone.
withTimeZone(tz)DateTimeKeeps the instant, moves the clock.
startOf(unit)DateTimeTruncates. Weeks start on Monday.
endOf(unit)DateTimeThe last millisecond of the unit.
diff(other, unit)numberSigned this − other.
compare(other)-1 | 0 | 1Orders by instant, ignoring zone.
equals(other)booleanSame instant and zone.
isBefore isAfterbooleanInstant comparison.
isBetween(start, end)booleanInclusive of both bounds.
format(pattern, locale?)stringToken formatting.
toLocaleString(locale?, options?)stringLocalised, via Intl.
toISOString() toJSON() toString()stringRound-trippable ISO form.
toDate()DateThe same instant as a Date.
toTemporal()Temporal.ZonedDateTimeRequires Temporal.

Units are 'year', 'month', 'week', 'day', 'hour', 'minute', 'second' and 'millisecond'.

Arithmetic

Calendar units (years, months, weeks, days) move the wall clock. Exact units (hours and below) move the instant. Across a DST boundary the two differ:

TypeScript
const dt = new DateTime('2026-03-07T12:00:00', { timeZone: 'America/New_York' });

dt.add({ days: 1 }).format('HH:mm');  //=> '12:00'  — 23 real hours later
dt.add({ hours: 24 }).format('HH:mm'); //=> '13:00' — exactly 24 hours later

Month arithmetic clamps rather than rolling over:

TypeScript
new DateTime('2026-01-31').add({ months: 1 }).format('yyyy-MM-dd'); //=> '2026-02-28'

diff counts whole units for year, month, week and day, truncated toward zero, and returns fractional elapsed time for hour and below.

Time zones and DST

A wall-clock time can be missing or repeated at a DST transition. disambiguation decides, matching Temporal:

TypeScript
const wall = '2026-03-08T02:30:00';           // never happens in New York
const timeZone = 'America/New_York';

new DateTime(wall, { timeZone }).format('HH:mm ZZ');
//=> '03:30 -04:00'  — 'compatible' shifts a gap forward

new DateTime(wall, { timeZone, disambiguation: 'earlier' }).format('HH:mm ZZ');
//=> '01:30 -05:00'

new DateTime(wall, { timeZone, disambiguation: 'reject' });
//=> throws RangeError

For an overlap — a wall time that happens twice — 'compatible' takes the earlier instant. 'reject' throws in both cases.

Format tokens

TokenOutputTokenOutput
yyyy yy2026 26HH Hhour, 0–23
MMMM MMM MM MJuly Jul 07 7hh hhour, 1–12
dd d29 29mm mminute
EEEE EEEWednesday Wedss ssecond
DDDday of yearSSSmillisecond
wwISO weekaAM / PM
zzzAsia/KolkataZZ Z+05:30 +0530

Text in single quotes is literal. Only month and weekday names depend on locale; everything else is locale-independent, so a pattern produces the same bytes on every runtime — safe for filenames, log lines and database columns.

TypeScript
const dt = new DateTime('2026-07-29T14:30:15.250', { timeZone: 'Asia/Kolkata' });

dt.format("yyyy-MM-dd'T'HH:mm:ss");  //=> '2026-07-29T14:30:15'
dt.format('EEEE, d MMMM yyyy');      //=> 'Wednesday, 29 July 2026'
dt.format('MMMM', 'fr-FR');          //=> 'juillet'
dt.toISOString();                    //=> '2026-07-29T14:30:15.250+05:30[Asia/Kolkata]'

toISOString round-trips through the constructor exactly, and toJSON uses it, so instances survive JSON.stringify.

Compared with other date libraries

@opentf/stddate-fns v4LuxonDay.jsMomentjs-joda
Paradigmimmutable classfunctions on Dateimmutable classimmutable wrappermutableimmutable class
Size, min + gzip4.6 KB~1–15 KB, per import~20 KB2 KB + plugins~20 KB + locales~40 KB
Tree-shakeableNo one classYes best in classNovia pluginsNoNo
IANA time zonesYes IntlYes @date-fns/tzYes IntlYes tz pluginYes moment-timezoneYes @js-joda/timezone
Bundles a tz databasenonononoyesyes
Runtime dependencieszerozerozerozerozeroseparate packages
Temporal precisionYes where availableNoNoNoNoNo own model
Strict parsingYes throwsYesYesNo looseNo looseYes
Invalid input yieldsthrowsInvalid Dateinvalid objectinvalid objectinvalid objectthrows
LocalesIntl~200 bundled modulesIntlbundled filesbundled filesseparate package
Statusnewactiveactiveactivemaintenanceactive

The @opentf/std figure is measured, not estimated: import { DateTime } from '@opentf/std' bundled with esbuild against a real install, minified and gzipped. The other columns are the sizes those projects publish for themselves.

Info

Closest in spirit to Luxon — an immutable class with Intl-backed zones and no bundled database — but with a much smaller API and Temporal precision where the runtime offers it.

Where this wins. Real IANA time-zone support with zero dependencies and no bundled database. Temporal precision, which no other library in the table has. Strict parsing that throws instead of handing back an invalid object that propagates silently. And it is already installed if you use @opentf/std.

Where it does not. DateTime is one class, so it cannot be tree-shaken the way date-fns can — importing it costs the full 4.6 KB even if you only format. date-fns also ships far more functions, including business-day maths and dozens of differenceIn* variants. Luxon and date-fns have absorbed a decade of DST and calendar bug reports; this is new. And there is no Duration or Interval type yet, so diff returns a plain number.

If bundle size dominates your decision, use date-fns. If you already depend on @opentf/std and want correct dates without another package, use this.

Runtime support

The whole API works on every runtime. Only the machinery underneath changes.

RuntimeBackend
Node.js 26+, Deno, ES-RuntimeTemporal
Chrome 144+, Edge 144+, Firefox 139+Temporal
Node.js 24 / 25 with --harmony-temporalTemporal
Node.js 22 and earlier, Bun, LLRT, SafariIntl
Warning

toTemporal and DateTime.fromTemporal are the only members that need Temporal, and they throw where it is absent. Check DateTime.hasTemporal first.

On LLRT, Intl ignores locales and component options, so month names, weekday names and toLocaleString do not work. Numeric tokens and all date arithmetic are unaffected.

Try it

Last updated on
Edit this page