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?
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.