Adapting a validated domain
Many apps already validate their domain at the boundary (Zod, valibot, a JSON body)
and hold typed objects. calendaryjs is built to receive them: validated dates are
plain string, and configs are plain objects — so the mapping is a pure function
with no as casts.
Your domain
Section titled “Your domain”import { z } from "zod";
// Validated at the edge (API, form, file).const Anniversary = z.object({ id: z.string(), name: z.string(), date: z.string(), // "YYYY-MM-DD" — a plain string, not a branded literal});type Anniversary = z.infer<typeof Anniversary>;Map to configs
Section titled “Map to configs”Write a pure domain → EventConfig. Config date fields accept
DateInput (any string), so the validated date
flows straight in — no cast:
import type { EventConfig } from "calendaryjs";
function toConfig(a: Anniversary): EventConfig { const [, month, day] = a.date.split("-"); return { type: "const", // fires every year on this month/day id: a.id, title: a.name, month: Number(month), day: Number(day), metadata: { source: "anniversary" }, // round-trips onto every occurrence };}
const configs = anniversaries.map(toConfig);calendary().addGroup({ id: "anniversaries", events: configs }); // no castPlugin events
Section titled “Plugin events”For a plugin type, type your mapper’s return as the plugin’s own event interface — or
use defineEvent for a quick inline config. Either
flows into addGroup / load without a cast:
import type { LunarEvent } from "calendaryjs-plugin-lunar";
const toLunar = (d: DomainLunar): LunarEvent => ({ type: "lunar", id: d.id, title: d.name, lunarMonth: d.month, lunarDay: d.day,});Dates in vs out
Section titled “Dates in vs out”Supply dates as plain strings — overrideDates, exceptions keys, and config date
fields all accept DateInput. Read them back as branded DateString on output
(event.date), where the engine guarantees YYYY-MM-DD. See
Event types.