Skip to content

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.

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>;

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 cast

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,
});

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.