DateOnly
DateOnly represents a date with no time and no time zone — a day on a calendar. It is the TypeScript counterpart of .NET’s System.DateOnly.
Why not a Date
Section titled “Why not a Date”A JavaScript Date is an instant, and a calendar date is not one. Turning 2026-05-12 into a Date produces UTC midnight, which every browser-local getter west of UTC reads back as the 11th:
// A birthday, a due date, an invoice date — none of them are instantsconst wrong = new Date('2026-05-12');console.log(wrong.getDate()); // 11 in New York, 12 in OsloThat is a wrong answer that looks right, and it is only wrong for users in some time zones — which is how it survives development in others. DateOnly holds the three parts the server sent, so there is no instant to convert and nothing to shift.
Creating
Section titled “Creating”Use parse() for the ISO-8601 form the server sends, from() for known parts, and fromDate() to take the calendar date a Date falls on.
import { DateOnly } from '@cratis/fundamentals';
const parsed = DateOnly.parse('2026-05-12');const constructed = DateOnly.from(2026, 5, 12);const today = DateOnly.fromDate(new Date());parse() accepts yyyy-MM-dd and throws an Error for anything else. fromDate() reads the local parts rather than the UTC ones, because the calendar date a moment falls on is a question only a time zone can answer, and the local one is the one the person looking at the screen is in.
Available values
Section titled “Available values”import { DateOnly } from '@cratis/fundamentals';
const date = DateOnly.parse('2026-05-12');
console.log(date.year); // 2026console.log(date.month); // 5 — 1 through 12, not 0-based like Dateconsole.log(date.day); // 12Note that month is 1 through 12, matching the wire format and .NET, rather than the 0-based month a Date uses.
Formatting
Section titled “Formatting”toString() produces the same ISO-8601 form the server sent.
import { DateOnly } from '@cratis/fundamentals';
const date = DateOnly.from(2026, 5, 12);console.log(date.toString()); // "2026-05-12"Converting to a Date
Section titled “Converting to a Date”Where a Date is genuinely wanted — to feed a date picker, or to do arithmetic — toDate() constructs one at midnight in the local time zone, so the date read back with getDate() is the one you started with.
import { DateOnly } from '@cratis/fundamentals';
const date = DateOnly.parse('2026-05-12');const asDate = date.toDate();console.log(asDate.getDate()); // 12, in every time zoneThis invents a time that was never sent, which is why it is a method to call rather than what the value is — the choice is visible at the call site making it.
Equality
Section titled “Equality”import { DateOnly } from '@cratis/fundamentals';
const first = DateOnly.parse('2026-05-12');const second = DateOnly.from(2026, 5, 12);
console.log(first.equals(second)); // trueconsole.log(first.equals(DateOnly.from(2026, 5, 13))); // falseconsole.log(first.equals(undefined)); // falseJSON serialization
Section titled “JSON serialization”DateOnlyJsonConverter is registered with JsonSerializer out of the box, so a field declared as DateOnly deserializes from the ISO-8601 string the server sends and serializes back to it.
import { DateOnly, field, JsonSerializer } from '@cratis/fundamentals';
export class Invoice { @field(String) number!: string;
@field(DateOnly) issued!: DateOnly;}
const invoice = JsonSerializer.deserialize(Invoice, '{"number":"INV-1","issued":"2026-05-12"}');console.log(invoice.issued.day); // 12
const json = JsonSerializer.serialize(invoice); // {"number":"INV-1","issued":"2026-05-12"}Serialization goes through JsonSerializer, not through JSON.stringify — DateOnly has no toJSON(), so JSON.stringify writes the individual parts instead of the ISO-8601 string.
Related
Section titled “Related”- TimeOnly — a time of day with no date and no time zone
- JsonSerializer — how types are converted on the way in and out
- @field Decorator — declaring the runtime type of a field