If you’re here, you probably already know what Valibot
(Github) is. But if not, the core function of Valibot
is to create a schema. A schema can be compared to a type definition in TypeScript. The big
difference is that TypeScript types are “not executed” and are more or less a DX feature. A schema
on the other hand, apart from the inferred type definition, can also be executed at runtime to
guarantee type safety of unknown data.
The Ultimate ISO Validator
The date regex here also validates the correct number of days in a month, including leap year.
seconds and milliseconds are optional. seconds must be true for milliseconds to be validated.
T is optional if date is not validated.
Time zone is optional, and accepts Z or a UTC offset (ex. +/-hh:mm)
ts
import type { PipeResult } from "valibot";/** * Creates a complete, customizable validation function that validates a datetime. * * The correct number of days in a month is validated, including leap year. * * Date Format: yyyy-mm-dd * Time Formats: [T]hh:mm[:ss[.sss]][+/-hh:mm] or [T]hh:mm[:ss[.sss]][Z] * * @param {Object} options The configuration options. * @param {boolean} options.date Whether to validate the date. * @param {boolean} options.time Whether to validate the time. * @param {boolean | "optional"} options.seconds Whether to validate the seconds. * @param {boolean | "optional"} options.milliseconds Whether to validate the milliseconds. * @param {boolean | "optional"} options.timezone Whether to validate the timezone. * @param {string} error The error message. * * @returns A validation function. */export function iso<TInput extends string>(options?: { date?: boolean; time?: boolean; seconds?: boolean | "optional"; milliseconds?: boolean | "optional"; timezone?: boolean | "optional"; error?: string;}) { return (input: TInput): PipeResult<TInput> => { const { date = true, time = true, seconds = "optional", milliseconds = "optional", timezone = "optional", error = "Invalid ISO string", } = options || {}; const dateRegex = "((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))"; const millisecondsRegex = milliseconds ? `(\\.\\d{3})${milliseconds === "optional" ? "?" : ""}` : ""; const secondsRegex = seconds ? `(:[0-5]\\d${millisecondsRegex})${seconds === "optional" ? "?" : ""}` : ""; const timezoneRegex = timezone ? `([+-]([01]\\d|2[0-3]):[0-5]\\d|Z)${timezone === "optional" ? "?" : ""}` : ""; const timeRegex = `([01]\\d|2[0-3]):[0-5]\\d${secondsRegex}${timezoneRegex}`; const regex = new RegExp(`^${date ? dateRegex : ""}${date && time ? "T" : time ? "T?" : ""}${time ? timeRegex : ""}$`); if (!regex.test(input)) { return { issues: [ { validation: "iso", message: error, input } ] }; } return { output: input }; };}
Valid Examples
ts
"2023-08-05T12:24:59.000Z"; // options undefined"2023-08-05T12:24:59.000-05:00"; // options undefined"2023-08-05T12:24-05:00"; // options undefined// seconds, milliseconds, and timezone are optional by default, or you can explicitly exclude them"2023-08-05T12:24"; // { seconds: false, timezone: false }// date and time are required by default, but you can exclude them"2023-08-05"; // { time: false }"T12:24:59.000Z"; // { date: false }// If date is excluded, the T is optional"12:24"; // { date: false, seconds: false, timezone: false }
The number of days in a month are also validated, including leap year.
2023-02-28, 2024-02-29, and 2023-06-30 are valid.
2023-02-29 and 2023-06-31 are invalid.
Using the Validator
Here is an example using the iso() validator. This date schema will validate the Date constructor
input and output a valid Date object.
ts
// Transforms a Date, string, or number into a Dateexport const dateSchema = transform( // Input types: Date, string, number union( [date(), string([iso()]), number([minValue(0)])], "Must be a valid Date object, ISO string, or UNIX timestamp" ), // Output type: Date (input) => new Date(input));
The Entire RegEx (Uncut)
This is the entire uncut regex. It behaves the same as the validator if no options were defined.
The built-in validator only uses the default value if the input is undefined. This one coerces the
value if the input is an empty string (trimmed), NaN, or falsy. I believe this fits more use cases
for a default value.
ts
export function withDefault<TSchema extends BaseSchema>(schema: TSchema, value: Input<TSchema>) { return coerce(schema, (input) => typeof value === "string" ? `${input}`.trim() || value : !input || (typeof value == "number" && isNaN(Number(input))) ? value : input );}