Timezone-safe date-only (YYYY-MM-DD) parsing and formatting for JavaScript and
TypeScript. Zero dependencies.
A "date-only" value, like an invoice date, a due date, or a date of birth, has no time and no timezone. It is just a label on the calendar. The obvious JavaScript code gets this wrong all the time:
new Date("2024-01-15").getDate()
// 14 in any negative-UTC-offset zone (most of the Americas)
// 15 in UTC or positive offsetsnew Date("2024-01-15") is parsed as midnight UTC, so once you read it back with
local getters in a zone like America/New_York or America/Los_Angeles it lands
on the day before. The same trap hits date.toISOString().slice(0, 10) when the
Date was built from local fields.
datelite sidesteps the whole problem. It represents a date as three plain
integers (year, month, day) and never runs a calendar date through a timezone
conversion. Parsing and formatting a YYYY-MM-DD string is exact in every
timezone.
npm install dateliteimport {
parse,
format,
addDays,
diffDays,
fromLocalDate,
toUTCDate,
} from "datelite";
// Parse is exact everywhere. No off-by-one, no matter the TZ.
const due = parse("2024-01-15");
due.day; // 15, always
// Format round-trips parse exactly.
format(due); // "2024-01-15"
// Date math is timezone-safe and DST-proof.
format(addDays(due, 30)); // "2024-02-14"
diffDays(parse("2024-02-14"), due); // 30
// Read the calendar day a user actually picked in their own timezone,
// instead of the UTC date.
const picked = new Date(2024, 0, 15, 9, 30); // local Jan 15
fromLocalDate(picked); // { year: 2024, month: 1, day: 15 }
// When you do need a Date (for an API), anchor it at UTC midnight and
// read it back with the UTC getters, never the local ones.
toUTCDate(due).toISOString(); // "2024-01-15T00:00:00.000Z"A PlainDate is { year: number; month: number; day: number } with a 1-based
month and day.
| Function | Description |
|---|---|
parse(input) |
Parse a strict "YYYY-MM-DD" string into a PlainDate. Throws on bad format or impossible dates (for example 2024-02-30). |
tryParse(input) |
Like parse but returns null instead of throwing. |
format(date) |
Format a PlainDate back to "YYYY-MM-DD". Round-trips parse exactly. |
fromLocalDate(date) |
Build a PlainDate from a Date using its local calendar fields (the day the user saw). |
fromUTCDate(date) |
Build a PlainDate from a Date using its UTC calendar fields. |
toUTCDate(date) |
Turn a PlainDate into a Date anchored at UTC midnight. |
addDays(date, days) |
Add a whole number of days (may be negative). Timezone-safe and DST-proof. |
diffDays(a, b) |
Whole-day difference a - b. |
compare(a, b) |
Negative, zero, or positive. Sortable. |
equals(a, b) |
True when two dates are the same calendar day. |
today() |
Today in the local timezone, as a PlainDate. |
Date is an instant in time, not a calendar date. Storing a date-only value in a
Date forces you to pick a timezone, and the default UTC parse plus local read is
the most common off-by-one-day bug in web apps. datelite keeps the value as a
calendar label until the moment you genuinely need an instant, and gives you
explicit fromLocalDate / fromUTCDate / toUTCDate conversions so the
direction is never ambiguous.
npm testThe tests are run under a negative-UTC timezone in CI so the round-trip guarantees are exercised in exactly the conditions where naive code breaks.
MIT. See LICENSE.