Reputation: 171321
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.
Upvotes: 18
Views: 8583
Reputation: 10907
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 astring
"
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:
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>);
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");
};
Upvotes: 3