'How to create a reusable react-hook-form component with TypeScript generics?

Person component is reused in two different forms:

Person.tsx

import { UseFormReturn } from "react-hook-form";
import { FieldPaths } from "./types";

type Props<TFormValues> = {
  methods: UseFormReturn<TFormValues>;
  fieldPaths: FieldPaths<TFormValues>;
};

const Person = <TFormValues,>({
  methods,
  fieldPaths
}: Props<TFormValues>) => {
  const { register } = methods;

  return (
    <div className="person">
      <div className="name-field">
        <input {...register(fieldPaths.firstName)} placeholder="First name" />
        <input {...register(fieldPaths.lastName)} placeholder="Last name" />
      </div>
      <div className="radio-group">
        <label>
          <input type="radio" value="free" {...register(fieldPaths.status)} />{" "}
          Free
        </label>
        <label>
          <input type="radio" value="busy" {...register(fieldPaths.status)} />{" "}
          Busy
        </label>
      </div>
    </div>
  );
};

export { Person };

SimpleForm.tsx

import { useForm } from "react-hook-form";
import { Person } from "./Person";
import { TPerson } from "./types";
import { emptyPerson } from "./utils";

type SimpleFormValues = {
  person: TPerson;
};

const simpleFormFieldPaths = {
  firstName: "person.name.first",
  lastName: "person.name.last",
  status: "person.status"
} as const;

const SimpleForm = () => {
  const methods = useForm<SimpleFormValues>({
    defaultValues: {
      person: emptyPerson
    }
  });
  const { handleSubmit } = methods;
  const onSubmit = ({ person }: SimpleFormValues) => {
    console.log(JSON.stringify(person, null, 2));
  };

  return (
    <div>
      <h2>Simple form</h2>
      <form method="post" onSubmit={handleSubmit(onSubmit)}>
        <Person methods={methods} fieldPaths={simpleFormFieldPaths} />
        <button type="submit">Submit</button>
      </form>
    </div>
  );
};

export { SimpleForm };

ComplexForm.tsx

import { useFieldArray, useForm } from "react-hook-form";
import { Person } from "./Person";
import { TPerson } from "./types";
import { emptyPerson } from "./utils";

type ComplexFormValues = {
  artists: TPerson[];
};

const ComplexForm = () => {
  const methods = useForm<ComplexFormValues>({
    defaultValues: {
      artists: [emptyPerson, emptyPerson, emptyPerson]
    }
  });
  const { control, handleSubmit } = methods;
  const { fields } = useFieldArray({
    control,
    name: "artists"
  });
  const onSubmit = ({ artists }: ComplexFormValues) => {
    console.log(JSON.stringify(artists, null, 2));
  };

  return (
    <div>
      <h2>Complex form</h2>
      <form method="post" onSubmit={handleSubmit(onSubmit)}>
        {fields.map((field, index) => {
          const complexFormFieldPaths = {
            firstName: `artists.${index}.name.first`,
            lastName: `artists.${index}.name.last`,
            status: `artists.${index}.status`
          } as const;

          return (
            <Person
              methods={methods}
              fieldPaths={complexFormFieldPaths}
              key={field.id}
            />
          );
        })}
        <button type="submit">Submit</button>
      </form>
    </div>
  );
};

export { ComplexForm };

types.ts

import { FieldPath } from "react-hook-form";

export type FieldPaths<TFormValues> = {
  firstName: FieldPath<TFormValues>;
  lastName: FieldPath<TFormValues>;
  status: FieldPath<TFormValues>;
};

export type TPerson = {
  name: {
    first: string;
    last: string;
  };
  status: "" | "free" | "busy";
};

utils.ts

import { TPerson } from "./types";

export const emptyPerson: TPerson = {
  name: {
    first: "",
    last: ""
  },
  status: ""
};

It works beautifully, but if I try to setValue in Person, TypeScript throws an error:

const { setValue } = methods;

setValue(fieldPaths.firstName, "David");

Argument of type 'string' is not assignable to parameter of type 'UnpackNestedValue<PathValue<TFormValues, Path>>'

How could I fix this?

CodeSandbox


I believe what I want is to hint TypeScript that PathValue(TFormValues, typeof fieldPaths.firstName) is always going to be a string, and then it should work, but I'm not sure how to achieve that.

Note: PathValue is defined here.



Solution 1:[1]

Looks like this used to be fixed in late react-hook-form@6; along had came v7 which reintroduced the problem and seems to have been carried into v8 beta as well.

Regarding your question

"hint TypeScript that PathValue(TFormValues, typeof fieldPaths.firstName) is always going to be a string"

This will likely not work, because of what setValue has been defined to accept.

Ideally we'd push the issue to the authors and hope for a speedy fix, but in the absence of that, there are two solutions here:

  1. Either alias UnpackNestedValue and cast to it

    import { UnpackNestedValue, PathValue, Path } from "react-hook-form";
    
    type ValueArgument<f> = UnpackNestedValue<PathValue<f, Path<f>>>;
    
    setValue(fieldPaths.firstName, "David" as ValueArgument<TFormValues>);
    
  2. Extend the types all the way up to accept primitives

    import {
      UseFormReturn,
      FieldValues,
      FieldPath,
      UnpackNestedValue,
      FieldPathValue,
      SetValueConfig,
    } from "react-hook-form";
    import { FieldPaths } from "./types";
    
    type UseMyFormSetValue<TFieldValues extends FieldValues> = <
      TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
    >(
      name: TFieldName,
      value: UnpackNestedValue<FieldPathValue<TFieldValues, TFieldName>> | string,
      options?: SetValueConfig
    ) => void;
    
    interface UseMyFormReturn<TFormValues> extends UseFormReturn<TFormValues> {
      setValue: UseMyFormSetValue<TFormValues>;
    }
    
    type Props<TFormValues> = {
      methods: UseMyFormReturn<TFormValues>;
      fieldPaths: FieldPaths<TFormValues>;
    };
    
    const Person = <TFormValues>({ methods, fieldPaths }: Props<TFormValues>) => {
      const { register, setValue } = methods;
    
      const changeName = () => {
        setValue(fieldPaths.firstName, "David");
      };
    

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 diedu