
Replacing Moment.js, date-fns, Luxon, and Day.js with Temporal
A practical migration cheatsheet: how to do formatting, parsing, arithmetic, diffing, comparison, and time zone conversion in Temporal, mapped directly to the Moment/date-fns/Luxon/dayjs calls you already know.
The previous article on Temporal covered why it exists and its core types. This one is the reference you actually reach for during a migration: the exact Temporal call for every common thing you’re doing today with Moment.js, date-fns, Luxon, or Day.js.
ℹ️ Setup
Where Temporal isn’t natively available yet, install the official polyfill: npm install @js-temporal/polyfill. Import it once as import {Temporal} from "@js-temporal/polyfill"; —
every example below uses the same global Temporal object either way.
Formatting
// Moment
moment().format("YYYY-MM-DD"); // "2026-09-15"
// date-fns
format(new Date(), "yyyy-MM-dd"); // "2026-09-15"
// Luxon
DateTime.now().toFormat("yyyy-MM-dd"); // "2026-09-15"
// Temporal — ISO output is built in, no format string needed for this case
Temporal.Now.plainDateISO().toString(); // "2026-09-15"
For anything locale-aware (weekday names, “Sep 15, 2026” style output), Temporal defers to Intl.DateTimeFormat instead of inventing its own format-string language:
const date = Temporal.Now.plainDateISO();
new Intl.DateTimeFormat("en-US", { dateStyle: "long" }).format(date);
// "September 15, 2026"
date.toLocaleString("en-US", { weekday: "long", month: "short", day: "numeric" });
// "Tuesday, Sep 15"
toLocaleString on any Temporal object forwards straight to Intl.DateTimeFormat — there’s no separate format-token syntax (yyyy, MMM, etc.) to learn or keep in sync with a library’s docs.
Parsing
// Moment — accepts near-anything, which is exactly the ambiguity problem
moment("2026-09-15T14:30:00");
// date-fns
parseISO("2026-09-15T14:30:00");
// Luxon
DateTime.fromISO("2026-09-15T14:30:00");
// Temporal — strict ISO 8601 / RFC 9557 only, throws on ambiguous input instead of guessing
Temporal.PlainDateTime.from("2026-09-15T14:30:00");
⚠ Temporal will not parse loose formats
Temporal.PlainDate.from("09/15/2026") throws a RangeError. This is deliberate — Temporal
refuses to guess whether that’s month/day or day/month. If you’re ingesting non-ISO strings (a CSV
export, a legacy API), parse them into an ISO string yourself first, the same way you’d validate
any other untrusted input.
Getting “now”
// Moment
moment();
// date-fns
new Date();
// Luxon
DateTime.now();
// Temporal — pick the type that matches what you actually need
Temporal.Now.plainDateISO(); // just today's date
Temporal.Now.plainDateTimeISO(); // today's date + local time, no zone
Temporal.Now.zonedDateTimeISO(); // instant + local time zone — what you usually want
Temporal.Now.instant(); // raw UTC instant, no calendar/zone
This is the biggest mental shift coming from any Date-based library: instead of one object for every use case, you pick the Temporal type that matches the question you’re actually asking. Reaching for zonedDateTimeISO() by default covers most “what time is it right now, here” use cases.
Adding and subtracting
// Moment
moment().add(3, "days").subtract(2, "hours");
// date-fns
subHours(addDays(new Date(), 3), 2);
// Luxon
DateTime.now().plus({ days: 3 }).minus({ hours: 2 });
// dayjs
dayjs().add(3, "day").subtract(2, "hour");
// Temporal
Temporal.Now.zonedDateTimeISO().add({ days: 3 }).subtract({ hours: 2 });
Chaining works the same way you’re used to from Luxon/dayjs, and every intermediate value is a new immutable instance — nothing here can accidentally mutate a shared object the way moment().add(...) mutates in place.
Difference between two dates
// Moment
moment(end).diff(moment(start), "hours");
// date-fns
differenceInHours(end, start);
// Luxon
end.diff(start, "hours").hours;
// Temporal — until()/since() return a Duration, not a raw number
start.until(end, { largestUnit: "hours" }); // Temporal.Duration
start.until(end, { largestUnit: "hours" }).hours; // just the number, if that's all you need
until/since return a full Temporal.Duration (hours, minutes, seconds all populated as needed), not a single float you have to remember the unit of — you ask for the unit breakdown you want via largestUnit/smallestUnit instead of calling a differently-named function per unit like differenceInHours / differenceInDays / differenceInMinutes.
Comparison
// Moment
moment(a).isBefore(b);
moment(a).isAfter(b);
moment(a).isSame(b);
// date-fns
isBefore(a, b);
isAfter(a, b);
isEqual(a, b);
// Luxon
a < b; // Luxon DateTime supports valueOf-based comparison
a.equals(b);
// Temporal
Temporal.PlainDate.compare(a, b); // -1, 0, or 1 — sort-friendly
a.equals(b);
Temporal.PlainDate.compare (and the equivalent compare static on every Temporal type) returns the same -1/0/1 shape Array.prototype.sort expects, so sorting a list of Temporal values is dates.sort(Temporal.PlainDate.compare) with no comparator function to write by hand.
Time zone conversion
// Moment (requires moment-timezone, a separate package)
moment.tz(date, "America/New_York").format();
// date-fns (requires date-fns-tz, a separate package)
formatInTimeZone(date, "America/New_York", "yyyy-MM-dd HH:mm:ssXXX");
// Luxon — built in
dt.setZone("America/New_York");
// Temporal — built in, no extra package regardless of which library you're replacing
zonedDateTime.withTimeZone("America/New_York");
This is the one where the gap is biggest: Moment and date-fns need a separate time-zone package (moment-timezone, date-fns-tz) just to do zone conversion correctly, because neither ships IANA time zone data by default. Temporal has this built in — withTimeZone is a method on ZonedDateTime, nothing extra to install.
Cheatsheet
| Task | Moment | date-fns | Luxon | Temporal |
|---|---|---|---|---|
| Now | moment() |
new Date() |
DateTime.now() |
Temporal.Now.zonedDateTimeISO() |
| Parse ISO string | moment(str) |
parseISO(str) |
DateTime.fromISO(str) |
Temporal.PlainDateTime.from(str) |
| Format | .format("YYYY-MM-DD") |
format(d, "yyyy-MM-dd") |
.toFormat("yyyy-MM-dd") |
.toLocaleString(locale, opts) |
| Add duration | .add(3, "days") |
addDays(d, 3) |
.plus({ days: 3 }) |
.add({ days: 3 }) |
| Subtract duration | .subtract(2, "hours") |
subHours(d, 2) |
.minus({ hours: 2 }) |
.subtract({ hours: 2 }) |
| Diff | .diff(other, "hours") |
differenceInHours(a, b) |
.diff(other, "hours") |
.until(other, { largestUnit: "hours" }) |
| Compare | .isBefore(other) |
isBefore(a, b) |
a < b |
Temporal.PlainDate.compare(a, b) |
| Change time zone | .tz("America/New_York") (needs moment-timezone) |
formatInTimeZone(...) (needs date-fns-tz) |
.setZone("America/New_York") |
.withTimeZone("America/New_York") |
| Mutability | Mutates in place | Immutable | Immutable | Immutable |
What doesn’t map 1:1
A few things worth knowing before you start swapping calls:
- No format-token strings. There’s no Temporal equivalent of
"YYYY-MM-DD HH:mm"— formatting goes throughIntl.DateTimeFormatoptions ({ year: "numeric", month: "2-digit", ... }) instead. For a handful of ISO-shaped outputs,toString()/toJSON()already give you what a format string would. - No lenient parsing. Any code relying on Moment’s “just figure it out” parsing of ambiguous strings needs an explicit conversion step before it reaches Temporal — see the warning above.
- Relative time (“3 days ago”) isn’t built into Temporal itself. Use
Intl.RelativeTimeFormatwith the numeric difference fromuntil/since, the same way you’d already combine a diff function with a formatting layer in date-fns.
FAQ
❓ Can I migrate incrementally, file by file?
Yes — Temporal objects convert cleanly to/from native Date
(Temporal.Instant.fromEpochMilliseconds(date.getTime()) and instant.epochMilliseconds), so a
Temporal-based module and a Moment/date-fns-based one can coexist while you migrate piece by piece
rather than all at once.
❓ Does this remove the need for date-fns-tz or moment-timezone specifically?
Yes — time zone conversion is one of Temporal’s built-in strengths, covered above. That’s usually the single biggest dependency-weight win from migrating, since IANA time zone data is what makes those add-on packages non-trivial in the first place.
❓ Is there a bundle-size reason to migrate, not just an API one?
Where Temporal is natively supported by your target runtimes, yes — it ships with the engine, so you drop the library’s bundle weight entirely. Until then, the polyfill has its own size cost, so the win is really about API correctness first and bundle size second, once native support lands.
Conclusion
The move from Moment/date-fns/Luxon/dayjs to Temporal is mostly a mechanical one, once you know the mapping: pick the Temporal type matching what you’re representing, swap format-string calls for Intl.DateTimeFormat/toLocaleString, and drop the separate time-zone package since withTimeZone is built in. The cheatsheet above covers the calls you’ll hit in the first pass through any real codebase.
Need this kind of migration done for you?
I work as a JavaScript/TypeScript developer and consultant through PAPION, based in Udine, Italy — for clients in Udine and Friuli, and remotely for teams across Italy and abroad. If you’re planning a library migration, or need React, Node.js, or TypeScript work more broadly, get in touch.
References

Co-Founder & CTO at PAPION. Senior full-stack engineer specializing in React, TypeScript, Node.js, and application security.