Skip to content

Plain vs Zoned

  • The core functions, which every industry uses.

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.

FamilyTypesNamespaceExample
PlainPlainDate, PlainTime, PlainDateTime, Duration@northguild/gmt/plain"2024-03-17T14:30:00"
ZonedZonedDateTime, 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.

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.000-04:00[America/New_York]" — default "compatible" rounds the
// nonexistent 2:30 AM forward. See DST Disambiguation for the full picture.

Plain and zoned cover most tasks. Two more namespaces handle fixed-point representations:

NamespaceTypeWhen to use
unixepoch seconds/millisecondsInterchange with systems, databases, JSON
utcUTC 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(); // 1710504645000 — milliseconds since epoch
getUtcNow(); // "2024-03-17T14:30:45Z" — absolute instant, UTC

These follow directly from the two-family split:

  1. No Date object. GMT uses Temporal types exclusively. Date is mutable, 0-indexed, and silently local — every property GMT was built to replace.
  2. String-in, string-out. Public APIs accept ISO 8601 strings and return strings, numbers, booleans, or arrays. No object handles cross the boundary.
  3. Invalid input returns a sentinel, never throws. "" for strings, null for numbers, false for booleans, [] for arrays. Check the sentinel; don’t catch.
  4. Wrap Temporal calls in try-catch. .from(), .add(), .since() throw RangeError on bad input — GMT’s public functions catch these and return the sentinel.