Changelog - 2026-10-15
TemporalHelper covers what a dayjs wrapper did
Feature Bug Fix Breaking ChangeIn one line. An app can call TemporalHelper directly instead of keeping its own date wrapper.
typescript
temporal.format({ value: row.paidAt, pattern: 'DD/MM/YYYY', fallback: '--' }); // '--' when paidAt is null
temporal.startOf({ value, unit: TemporalUnits.QUARTER });
temporal.set({ value, hour: 9, minute: 0 });
temporal.isBetween({ value, from, to, inclusivity: '[]' });
temporal.formatDuration({ milliseconds, pattern: 'mm:ss' });
const english = temporal.clone({ locale: 'en-US' });
temporal.fromWallClock({ value: pickedDay }); // the picked day, kept in the helper's zoneWhat changed
| Area | Now |
|---|---|
null | Bug fix. toDate, compare and format threw nothing and answered 1970 for null, and isValid(null) was true. Now null is a 400, and isValid is false |
| Reading without throwing | tryToDate({ value }) answers null; format({ ..., fallback }) writes the fallback for a value that is not a date. The constructor takes a fallback too, used by format and formatIntl when a call passes none |
| Quarter | TemporalUnits.QUARTER for add, startOf, endOf, diff, compare and relative; the Q token writes 1-4 |
set | Replaces wall-clock fields in the zone; the day clamps to the month |
| Comparisons | isBefore, isAfter, isSame, isSameOrBefore, isSameOrAfter, and isBetween with (), [], [) or (] |
| Durations | formatDuration takes a clock-face pattern (HH:mm:ss, mm:ss) and maxUnits |
| Locales | clone({ locale, tz }); the constructor's names replaces Intl's month and weekday names and AM/PM marker per locale, and a call in another tag of the same language uses the entry; the dd token writes the narrowest weekday |
| Date pickers | fromWallClock({ value }) moves a picker's host-zone wall clock into the helper's zone; toWallClock feeds it back; TemporalHelper.getHostTz() names the host's zone |
add | An hour or smaller takes a fraction: 1.5 hours is 90 minutes. A calendar unit still takes whole numbers only |
| The zone option | Breaking. timeZone is now tz everywhere on TemporalHelper and ITemporalAdapter: the constructor, every per-call option, getTz(), TemporalHelper.getHostTz(), DEFAULT_TEMPORAL_TZ, and getOffsetMilliseconds({ value, tz }). Intl's own timeZone option is unchanged |
| Structure | ITemporalHelper (the contract), AbstractTemporalHelper (the zone machinery) and TemporalHelper (the operations). The internals are classes in scope folders: TemporalCalendar with one shift method, TemporalTokens, TemporalPattern, TemporalLocale, TemporalDuration |
diff | Behavior change. Counted as dayjs counts: days by the wall clocks, months anchored on the later day of the month. diff(a, b) is always -diff(b, a) |
Who is affected
- Every caller that passes a zone. Rename
timeZonetotz:new TemporalHelper({ tz: 'Asia/Ho_Chi_Minh' }),format({ value, tz: 'UTC' }),getTimeZone()togetTz(). A custom adapter readsopts.tz. - Code that passed
nulland got 1970. It now gets a 400. UsetryToDate,isValidorformat({ fallback })where a date may be missing. - A
diffin months or years that ends on a month end. A few month-end cases change by one. Example: from 2026-02-28 back to 2024-02-29 was -23 months and is now -24, as dayjs answers. It matched 2,160 of 2,160 dayjs answers on a set of month-end and day-edge instants. - Everyone else. No change.
Files: packages/helpers/src/modules/temporal/helper.ts, packages/helpers/src/modules/temporal/calendar/calendar.ts, packages/helpers/src/modules/temporal/pattern/tokens.ts, packages/helpers/src/modules/temporal/duration/duration.ts