Skip to content

Instantly share code, notes, and snippets.

@kensleDev
Created June 28, 2025 19:05
Show Gist options
  • Select an option

  • Save kensleDev/c4ee39a4b13db4076d6a14846d4facb5 to your computer and use it in GitHub Desktop.

Select an option

Save kensleDev/c4ee39a4b13db4076d6a14846d4facb5 to your computer and use it in GitHub Desktop.
Curried zod validators
// validations.ts
import { z, ZodString, ZodNumber, ZodType, ZodTypeAny } from "zod";
// --- Types ---
/** A validator that takes a label and returns a function that takes and returns a Zod schema */ export type LabeledRule<T extends ZodTypeAny> = (label: string) => (schema: T) => T;
// --- Utility ---
/** Compose multiple labeled rules into one schema builder, injecting the label into each */ export function compose<T extends ZodTypeAny>( label: string, ...rules: LabeledRule<T>[] ): (base: T) => T { return (base: T) => rules.reduce((schema, rule) => rule(label)(schema), base); }
// --- String Validators ---
export const minLength = (min: number, message?: string): LabeledRule<ZodString> => (label) => (schema) => schema.refine((val) => val.length >= min, { message: message || ${label} must be at least ${min} characters, });
export const maxLength = (max: number, message?: string): LabeledRule<ZodString> => (label) => (schema) => schema.refine((val) => val.length <= max, { message: message || ${label} must be at most ${max} characters, });
export const regex = (pattern: RegExp, message?: string): LabeledRule<ZodString> => (label) => (schema) => schema.refine((val) => pattern.test(val), { message: message || ${label} is invalid, });
export const noSpaces = (message?: string): LabeledRule<ZodString> => (label) => (schema) => schema.refine((val) => !val.includes(" "), { message: message || ${label} must not contain spaces, });
export const email = (message?: string): LabeledRule<ZodString> => (label) => (schema) => schema.email({ message: message || ${label} must be a valid email address });
export const url = (message?: string): LabeledRule<ZodString> => (label) => (schema) => schema.url({ message: message || ${label} must be a valid URL });
// --- Number Validators ---
export const minValue = (min: number, message?: string): LabeledRule<ZodNumber> => (label) => (schema) => schema.refine((val) => val >= min, { message: message || ${label} must be at least ${min}, });
export const maxValue = (max: number, message?: string): LabeledRule<ZodNumber> => (label) => (schema) => schema.refine((val) => val <= max, { message: message || ${label} must be at most ${max}, });
export const positive = (message?: string): LabeledRule<ZodNumber> => (label) => (schema) => schema.positive({ message: message || ${label} must be a positive number });
export const nonNegative = (message?: string): LabeledRule<ZodNumber> => (label) => (schema) => schema.nonnegative({ message: message || ${label} must be zero or greater });
export const integer = (message?: string): LabeledRule<ZodNumber> => (label) => (schema) => schema.int({ message: message || ${label} must be an integer });
// --- Common Aliases ---
export const requiredString = (label: string) => compose(label, minLength(1))(z.string());
export const requiredNumber = (label: string) => compose(label, nonNegative())(z.number());
// --- Generic Required ---
export const required = (message?: string): LabeledRule<ZodTypeAny> => (label) => (schema) => schema.refine((val) => val !== undefined && val !== null && val !== "", { message: message || ${label} is required, });
// --- Example Auth Schemas ---
export const loginSchema = z.object({ email: compose("Email", required(), email())(z.string()), password: compose("Password", required(), minLength(8))(z.string()), });
export const registerSchema = z.object({ email: compose("Email", required(), email())(z.string()), password: compose("Password", required(), minLength(8))(z.string()), confirmPassword: compose("Confirm Password", required(), minLength(8))(z.string()), }).refine((data) => data.password === data.confirmPassword, { path: ["confirmPassword"], message: "Passwords must match", });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment