interface FormOptions<TBody, TResult> {
schema?: FormSchema<TBody>;
initial?: DeepPartial<TBody> | (() => DeepPartial<TBody>);
onSuccess?: (result: TResult) => void;
onError?: (errors: Record<string, string>) => void;
resetOnSuccess?: boolean;
revalidateOn?: 'blur' | 'change' | 'submit';
}
// Types below show the developer-facing API — the compiler auto-unwraps
// reactive properties, so you use them as plain values in JSX and templates.
type FormInstance<TBody, TResult> = FormBaseProperties<TBody> & NestedFieldAccessors<TBody>;
interface FormBaseProperties<TBody> {
action: string;
method: string;
onSubmit: (e: Event) => Promise<void>;
reset: () => void;
setFieldError: (field: FieldPath<TBody>, message: string) => void;
submit: (formData?: FormData) => Promise<void>;
submitting: boolean;
dirty: boolean;
valid: boolean;
fields: FieldNames<TBody>;
}
interface FieldState<T = unknown> {
value: T;
error: string | undefined;
dirty: boolean;
touched: boolean;
setValue: (value: T) => void;
reset: () => void;
}
// Recursive field accessors — nested objects get both FieldState
// and nested accessors (e.g., `form.address.city.error`).
// Simplified — the real type also handles arrays (numeric index access),
// built-in objects (Date, File, Blob treated as leaves), and detects
// reserved field name conflicts at compile time.
type NestedFieldAccessors<T> = {
[K in keyof T]: T[K] extends Record<string, unknown>
? FieldState<T[K]> & NestedFieldAccessors<T[K]>
: FieldState<T[K]>;
};
// Recursive dot-path type for nested fields (e.g., 'address.city')
type FieldPath<T, Prefix extends string = ''> =
| `${Prefix}${keyof T & string}`
| {
[K in keyof T & string]: T[K] extends Record<string, unknown>
? FieldPath<T[K], `${Prefix}${K}.`>
: never;
}[keyof T & string];
// Maps each field name to itself — for type-safe `name` attributes
type FieldNames<TBody> = { readonly [K in keyof TBody & string]: K };
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends Array<infer U>
? Array<DeepPartial<U>>
: T[K] extends Record<string, unknown>
? DeepPartial<T[K]>
: T[K];
};
interface FormSchema<T> {
parse(data: unknown): { ok: true; data: T } | { ok: false; error: unknown };
}