UNPKG

@abyrd9/zod-form-data

Version:

A Zod-based form data validation library

169 lines (161 loc) 5.71 kB
import { z } from "zod/v4"; import { $ZodType } from "zod/v4/core"; export type FieldProps<T> = { name: string; value: T; onChange: (payload: T | undefined | null) => void; error: string | null; }; export type NestedFields<T extends $ZodType> = T extends { _zod: { def: { type: "object"; shape: infer Shape } } } ? { [K in keyof Shape]: NestedFields<Shape[K] & $ZodType> } : T extends { _zod: { def: { type: "record"; valueType: infer Value } } } ? Record<string, NestedFields<Value & $ZodType>> : T extends { _zod: { def: { type: "map"; valueType: infer Value } } } ? Map<string, NestedFields<Value & $ZodType>> : T extends { _zod: { def: { type: "array"; element: infer Item } } } ? NestedFields<Item & $ZodType>[] : T extends { _zod: { def: { type: "set"; element: infer Item } } } ? Set<NestedFields<Item & $ZodType>> : T extends { _zod: { def: { type: "tuple"; items: infer Items } } } ? { [K in keyof Items]: NestedFields<Items[K] & $ZodType> } : T extends { _zod: { def: { type: "optional"; innerType: infer Inner } } } ? NestedFields<Inner & $ZodType> : T extends { _zod: { def: { type: "default"; innerType: infer Inner } } } ? NestedFields<Inner & $ZodType> : T extends { _zod: { def: { type: "nullable"; innerType: infer Inner } } } ? NestedFields<Inner & $ZodType> : T extends { _zod: { def: { type: "union"; options: infer Options } } } ? Options extends readonly $ZodType[] ? NestedFields<Options[number] & $ZodType> : FieldProps<z.infer<T>> : T extends { _zod: { def: { type: "intersection"; left: infer Left; right: infer Right } } } ? NestedFields<Left & $ZodType> & NestedFields<Right & $ZodType> : T extends { _zod: { def: { type: "lazy"; getter: () => infer LazyType } } } ? LazyType extends $ZodType ? NestedFields<LazyType> : FieldProps<z.infer<T>> : T extends { _zod: { def: { type: "transform"; input: infer Input; output: infer Output } } } ? NestedFields<Input & $ZodType> : T extends { _zod: { def: { type: "pipe"; input: infer Input; output: infer Output } } } ? NestedFields<Input & $ZodType> : T extends { _zod: { def: { type: "catch"; innerType: infer Inner } } } ? NestedFields<Inner & $ZodType> : T extends { _zod: { def: { type: "success"; data: infer Data } } } ? FieldProps<Data> : T extends { _zod: { def: { type: "literal"; value: infer Value } } } ? FieldProps<Value> : T extends { _zod: { def: { type: "enum"; values: infer Values } } } ? FieldProps<Values[keyof Values]> : T extends { _zod: { def: { type: "promise"; unwrap: infer Unwrapped } } } ? Unwrapped extends $ZodType ? NestedFields<Unwrapped> : FieldProps<z.infer<T>> : T extends { _zod: { def: { type: "custom" } } } ? FieldProps<z.infer<T>> : FieldProps<z.infer<T>>; // Helper function to get field props from a Zod v4 schema export function getFieldProps<T extends $ZodType>( field: T, path: string[] = [], flattenedData: Record<string, unknown> = {}, setFlattenedData?: (updater: (prev: Record<string, unknown>) => Record<string, unknown>) => void, errors?: Map<string, string> ): NestedFields<T> { const def = field._zod.def; const currentPath = path.join("."); const buildLeaf = (): FieldProps<z.infer<T>> => { const name = currentPath; const value = (flattenedData?.[name] ?? undefined) as z.infer<T> | undefined | null; const error = errors?.get(name) ?? null; const onChange = (payload: z.infer<T> | undefined | null) => { if (!setFlattenedData || !name) return; setFlattenedData((prev) => ({ ...prev, [name]: payload })); }; return { name, value, onChange, error } as FieldProps<z.infer<T>>; }; switch (def.type) { case "object": { if (field instanceof z.ZodObject) { const objectFields: Record<string, unknown> = {}; if (field.shape) { for (const [key, subField] of Object.entries(field.shape)) { objectFields[key] = getFieldProps( subField as $ZodType, [...path, key], flattenedData, setFlattenedData, errors ); } } return objectFields as NestedFields<T>; } break; } case "array": { if (field instanceof z.ZodArray) { const prefix = currentPath; const indices = new Set<number>(); for (const key of Object.keys(flattenedData ?? {})) { const match = prefix ? key.match(new RegExp(`^${prefix}\.(\\d+)`)) : null; if (match) indices.add(Number.parseInt(match[1], 10)); } const result: unknown[] = []; const sorted = Array.from(indices).sort((a, b) => a - b); if (sorted.length === 0) return result as NestedFields<T>; for (const index of sorted) { const sub = getFieldProps( field.element as $ZodType, [...path, String(index)], flattenedData, setFlattenedData, errors ); result.push(sub as unknown); } return result as NestedFields<T>; } break; } case "optional": { if (field instanceof z.ZodOptional) { return getFieldProps( field.def.innerType as $ZodType, path, flattenedData, setFlattenedData, errors ) as unknown as NestedFields<T>; } break; } case "default": { if (field instanceof z.ZodDefault) { return getFieldProps( field.def.innerType as $ZodType, path, flattenedData, setFlattenedData, errors ) as unknown as NestedFields<T>; } break; } case "nullable": { if (field instanceof z.ZodNullable) { return getFieldProps( field.def.innerType as $ZodType, path, flattenedData, setFlattenedData, errors ) as unknown as NestedFields<T>; } break; } default: { return buildLeaf() as unknown as NestedFields<T>; } } return buildLeaf() as unknown as NestedFields<T>; }