Types

Validator

The only exported type.

type Validator<Element> = Falsy | ((el: Element) => ValidatorResponse);

The element type is a parameter, so annotate the element you are actually attaching to and get a typed argument:

const minLength: Validator<HTMLInputElement> = el =>
  el.value.length < 5 && 'Use at least 5 characters';

const hasSelection: Validator<HTMLSelectElement> = el =>
  el.selectedIndex === 0 && 'Pick an option';

Falsy is part of the union so a conditional entry type-checks without a cast:

<input ref={validate(() => [required, needsMatch() && mustMatch])} />

Falsy

type Falsy = false | 0 | '' | null | undefined | void;

Everything that counts as passing. void is in the union so a validator that just runs if (bad) return message type-checks without an explicit return undefined.

ValidatorResponse

type ValidatorResponse = MaybePromise<string | Falsy>;

A message, nothing, or a promise of either.

Typing errors

useForm<ErrorFields> sets the shape of the store:

type SignupFields = {
  email: string;
  password: string;
  form: string;
};

const { errors } = useForm<SignupFields>();

Include your form-level keys in the type. They are not tied to any field, but they are keys in the same store, and leaving them out makes errors.form a type error.

validateField and getFieldValue both take keyof ErrorFields, so a form-level key is accepted there too even though no element is registered under it. Both are defined to handle a missing field: validateField resolves false and getFieldValue returns undefined.