Настройка типизации формы React Hook Form (≥ v7.44.0) + Zod с разными входными и выходными типами

Страницы:  1

Ответить
 

Professor Seleznov


Работая с формой, часто нам нужно сделать так, чтобы на вход она принимала данные одного типа, а после валидации их тип меняется
Моя форма состоит из полей, начальное значение которых - пустая строка, а после валидации - число
Давайте попробуем создать схему для такой формы и вывести из нее тип
const EMPTY_NUMERIC = '';
const numericScheme = z.union([z.number(), z.literal('')]);
type NumericValue = z.infer<typeof numericScheme>;
const requiredNumberWithRefine = numericScheme.refine(
(val) => val !== EMPTY_NUMERIC,
{
message: 'Это поле обязательно для заполнения',
},
);
export const formSchema = z.object({
amount: requiredNumberWithRefine,
quantity: requiredNumberWithRefine,
});
export type FormDataInfer = z.infer<typeof formSchema>;
Стандартный способ с infer не подойдет в таком случае, при попытке типизировать форму и задать начальные значения в хуке useForm посыпятся ошибки типизации
pic
Начиная с v7.44.0 (релиз) React Hook Form хук useForm стал выглядеть так
useForm<TFieldValues extends FieldValues = FieldValues, TContext = any, TTransformedValues = TFieldValues>
Появился 3-ий дженерик TTransformedValues - он определяет выходные параметры формы, после их модификации
В нашей ситуации мы можем сузить наш начальный тип NumericValue до number с помощью одного из методов - refine, transform, superRefine с pipe. Мы уже используем refine в схеме requiredNumberWithRefine, он подходит, но приведу примеры c transform и superRefine+pipe*, если, например, вы захотите использовать контекст
* если использовать только superRefine, тип не будет сужен
const requiredNumberWithTransform = numericScheme.transform(
(val: NumericValue, ctx: z.RefinementCtx) => {
if (val === EMPTY_NUMERIC) {
ctx.addIssue({
code: 'custom',
message: 'Это поле обязательно для заполнения',
});
return z.NEVER;
}
return val;
},
);
const requiredNumberWithSuperRefineAndPipe = numericScheme
.superRefine((val, ctx) => {
if (val === EMPTY_NUMERIC) {
ctx.addIssue({
code: 'custom',
message: 'Это поле обязательно для заполнения',
});
}
})
.pipe(z.number());
Далее выводим 2 отдельных типа FormDataInput и FormDataOutput с помощью дженериков  z.input и z.output 
export type FormDataInput = z.input<typeof formSchema>;
export type FormDataOutput = z.output<typeof formSchema>;
Если мы посмотрим, какие типы получились, то увидим
pic
pic
Теперь типизируем defaultValues с помощью FormDataInput, используем эти дженерики в хуке useForm (если мы не добавим типы в дженерики явно, они будут выведены самостоятельно, но давайте сделаем это для наглядности), в хендлере onSubmit используем FormDataOutput
Выглядеть это будет так:
const defaultValues: FormDataInput = {
amount: '',
quantity: '',
};
const Form = () => {
const {
handleSubmit,
control,
formState: { errors },
} = useForm<FormDataInput, unknown, FormDataOutput>({
resolver: zodResolver(formSchema),
defaultValues,
});
const onSubmit = (data: FormDataOutput) => {
saveData(data);
};
return (
<form
onSubmit={handleSubmit(onSubmit)}
...
Все, теперь явно видно какие параметры получает форма на входе и выходе, плюс мы можем отделять части формы в отдельные константы, файлы и безболезненно их типизировать -Источник
 
Loading...
Error