Go-inspired time utilities for Node.js with TypeScript types and zero runtime dependencies.
go-time brings familiar Go time concepts to JavaScript and TypeScript: durations, layouts, parsing/formatting, locations, timers, and tickers.
Precision note: go-time is built on JavaScript Date, so wall-clock time values are limited to millisecond precision.
- Go-style duration values and parser (
2h45m10.5s) TimeAPI for millisecond-precision arithmetic, comparisons, rounding, truncation, and calendar fields- Monotonic clock support for stable elapsed-time measurement with
now(),sub(),since(), anduntil() - Layout-based parse/format helpers (
RFC3339,DateTime,Kitchen, and more) - Time zone support with UTC, local, fixed offsets, and IANA zone names
- Timer and ticker primitives inspired by Go channels
- ESM-first package with generated TypeScript declarations
- No runtime dependencies
npm install @ahxar/go-time- Node.js 20+
import {
DateTime,
Millisecond,
Month,
RFC3339,
date,
now,
parse,
since,
sleep,
unixMilli,
} from "@ahxar/go-time";
const started = now();
const timeout = 250n * Millisecond;
await sleep(timeout);
const elapsed = since(started);
console.log(elapsed.toString()); // e.g. "252.3ms"
const t1 = parse(RFC3339, "2026-04-08T09:10:11.123Z");
console.log(t1.millisecond()); // 123
const t2 = date(2026, Month.April, 8, 12, 30, 0, 0);
console.log(t2.format(DateTime)); // "2026-04-08 12:30:00"
const t3 = unixMilli(1_700_000_000_123n);
console.log(t3.unixMilli()); // 1700000000123nimport { Hour, Minute, Second, parseDuration } from "@ahxar/go-time";
const a = parseDuration("2h45m10.5s");
const b = parseDuration("90s");
const c = parseDuration("1h30m");
console.log(a.milliseconds()); // 9910500n
console.log(b.round(Second).toString()); // "1m30s"
console.log(c.truncate(Minute).toString()); // "1h30m0s"Supported units: ms, s, m, h.
Time uses JavaScript Date under the hood, so parsing, formatting, constructors, and current-time reads only support millisecond precision.
import {
DateOnly,
DateTime,
Kitchen,
RFC3339,
parse,
parseInLocation,
fixedZone,
} from "@ahxar/go-time";
const t = parse(RFC3339, "2026-04-08T09:10:11.123Z");
console.log(t.format(DateOnly)); // "2026-04-08"
console.log(t.format(DateTime)); // "2026-04-08 09:10:11"
console.log(t.format(Kitchen)); // "9:10AM"
console.log(t.format(RFC3339)); // "2026-04-08T09:10:11.123Z"
const plus2 = fixedZone("PLUS2", 2 * 3600);
const localClock = parseInLocation(DateTime, "2026-04-08 12:00:00", plus2);
console.log(localClock.zone()); // { name: "PLUS2", offsetSeconds: 7200 }before(), after(), and equal() compare the underlying UTC instant, so the location attached to a Time value does not affect the result.
import { RFC3339, fixedZone, parse } from "@ahxar/go-time";
const utc = parse(RFC3339, "2026-04-08T12:00:00Z");
const plus2 = fixedZone("PLUS2", 2 * 3600);
const same = utc.in(plus2); // same instant, displayed as 14:00 PLUS2
console.log(utc.equal(same)); // true — same UTC instant
console.log(utc.before(same)); // false
console.log(utc.after(same)); // falseRounding and truncation to the minute:
import { DateTime, Minute, Month, UTC, date } from "@ahxar/go-time";
const at30s = date(2026, Month.April, 8, 12, 34, 30, 0, UTC);
console.log(at30s.format(DateTime)); // "2026-04-08 12:34:30"
console.log(at30s.truncate(Minute).format(DateTime)); // "2026-04-08 12:34:00"
console.log(at30s.round(Minute).format(DateTime)); // "2026-04-08 12:35:00"now() records both wall-clock time and a monotonic reading. When two Time values both carry monotonic data, sub(), since(), and until() use the monotonic clock for elapsed-time calculations instead of wall-clock time.
This makes elapsed-time measurement stable across wall-clock adjustments. Times created with unix(), unixMilli(), date(), or parse() do not include monotonic data.
import { Millisecond, Second, now, since, sleep, unix } from "@ahxar/go-time";
const started = now();
await sleep(25n * Millisecond);
const elapsed = since(started);
console.log(elapsed.milliseconds()); // about 25
const a = now();
const b = a.add(Second);
console.log(b.sub(a).seconds()); // 1
const wallOnly = unix(1_700_000_000n);
console.log(typeof wallOnly.unix()); // "bigint"
// `wallOnly` has no monotonic reading, so subtraction falls back to wall-clock time.import { Local, UTC, fixedZone, loadLocation, now } from "@ahxar/go-time";
const t = now();
const utc = t.in(UTC);
const local = t.in(Local);
const est = t.in(fixedZone("EST", -5 * 3600));
const berlin = t.in(loadLocation("Europe/Berlin"));
console.log(utc.zone()); // { name: "UTC", offsetSeconds: 0 }
console.log(local.zone()); // e.g. { name: "Local", offsetSeconds: -25200 }
console.log(est.zone()); // { name: "EST", offsetSeconds: -18000 }
console.log(berlin.zone()); // e.g. { name: "Europe/Berlin", offsetSeconds: 7200 }import { Millisecond, newTicker, newTimer } from "@ahxar/go-time";
const timer = newTimer(50n * Millisecond);
const firedAt = await timer.C.recv();
console.log(firedAt.unixMilli()); // e.g. 1775649011123n
const ticker = newTicker(100n * Millisecond);
let count = 0;
for await (const tick of ticker.C) {
console.log(tick.unixMilli()); // e.g. 1775649011223n
count += 1;
if (count === 3) {
ticker.stop();
break;
}
}Install dependencies:
npm installAvailable scripts:
npm run build- compile the publishable package todist/npm run check- TypeScript type checks without emitting filesnpm run fmt- format supported files with Oxcnpm run fmt:check- verify formatting without writing changesnpm run lint- run Oxlint across the repositorynpm run lint:fix- run Oxlint with auto-fixesnpm test- run the Vitest suite directly from the TypeScript test files
Contributions are welcome.
- Fork the repository.
- Create a feature branch.
- Add or update tests in
test/. - Run
npm run lint,npm run check, andnpm test. - Open a pull request describing your changes.
MIT. See LICENSE.