Plain vs Zoned
GMT’s API is built on two families of Temporal types that are strictly separate. Mixing them is the single most common source of silent bugs, so the library is designed to make the boundary impossible to cross by accident.
The two families
Section titled “The two families”| Family | Types | Namespace | Example |
|---|---|---|---|
| Plain | PlainDate, PlainTime, PlainDateTime, Duration |
@northguild/gmt/plain |
"2024-03-17T14:30:00" |
| Zoned | ZonedDateTime, Instant |
@northguild/gmt/zoned |
"2024-03-17T14:30:00-04:00[America/New_York]" |
- Plain values are calendar math without a timezone.
"2024-03-17T14:30:00"is just numbers on a clock face — it has no instant behind it until you attach a zone. - Zoned values carry an IANA timezone identifier.
"2024-03-17T14:30:00-04:00[America/New_York]"is a real, absolute instant. The bracketed zone is part of the value, not metadata.
Why they never mix
Section titled “Why they never mix”A plain datetime and a zoned datetime answer different questions:
- Plain: “What does the clock say?” — useful for arithmetic that should ignore timezones entirely (add 30 days, what’s the start of this month).
- Zoned: “When, in absolute time, does this happen?” — useful for scheduling, logging, and anything that must be correct across timezone boundaries.
GMT enforces this at the namespace level. addDate operates on plain strings;
addZoned operates on zoned strings. There is no function that silently accepts
both. If you need to move between them, use an explicit conversion:
import { convertPlainDateTimeToZoned } from "@northguild/gmt/zoned";
// Attach a timezone to a plain value — this is where DST disambiguation matters.convertPlainDateTimeToZoned("2024-03-10T02:30:00", "America/New_York");// "2024-03-10T03:30:00-04:00[America/New_York]" — default "compatible" rounds the// nonexistent 2:30 AM forward. See DST Disambiguation for the full picture.The other namespaces
Section titled “The other namespaces”Plain and zoned cover most tasks. Two more namespaces handle fixed-point representations:
| Namespace | Type | When to use |
|---|---|---|
unix |
epoch seconds/milliseconds | Interchange with systems, databases, JSON |
utc |
UTC instants ("…Z") |
When you need an absolute instant without a named zone |
import { getUnixNow } from "@northguild/gmt/unix";import { getUtcNow } from "@northguild/gmt/utc";
getUnixNow(); // 1710504645 — seconds since epochgetUtcNow(); // "2024-03-17T14:30:45Z" — absolute instant, UTCThe design rules
Section titled “The design rules”These follow directly from the two-family split:
- No
Dateobject. GMT uses Temporal types exclusively.Dateis mutable, 0-indexed, and silently local — every property GMT was built to replace. - String-in, string-out. Public APIs accept ISO 8601 strings and return strings, numbers, booleans, or arrays. No object handles cross the boundary.
- Invalid input returns a sentinel, never throws.
""for strings,nullfor numbers,falsefor booleans,[]for arrays. Check the sentinel; don’t catch. - Wrap Temporal calls in
try-catch..from(),.add(),.since()throwRangeErroron bad input — GMT’s public functions catch these and return the sentinel.
See also
Section titled “See also”- DST Disambiguation — what happens when a plain time is attached to a zone
- Zoned Operations — working with timezone-aware values
- Core Rules — the full statement of GMT’s contract