reactjs - كيف تنشئ مكون react-hook-form قابل لإعادة الاستخدام باستخدام النوعيات العامة في TypeScript؟


يتم إعادة استخدام مكون Person في شكلين مختلفين:

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: ""
};

يعمل بشكل رائع، لكن إذا حاولت setValue في Person، فإن TypeScript يعطي خطأ:

const { setValue } = methods;

setValue(fieldPaths.firstName, "David");

حجة من نوع 'string' غير قابلة للتعيين إلى معلمة من نوع 'UnpackNestedValue>'

كيف يمكنني إصلاح هذا؟

CodeSandbox


أعتقد أن ما أريده هو الإشارة إلى TypeScript بأن PathValue(TFormValues, typeof fieldPaths.firstName) سيكون دائمًا string، ومن ثم يجب أن يعمل، لكنني لست متأكدًا من كيفية تحقيق ذلك.

ملاحظة: PathValue معرف هنا.

الإجابة رقم 1

يبدو أن هذا كان قد تم إصلاحه في نهاية react-hook-form@6؛ ثم جاء الإصدار v7 الذي أعاد ظهور المشكلة ويبدو أنها استمرت أيضًا في إصدار v8 beta.

بخصوص سؤالك

أوجِه TypeScript بأن PathValue(TFormValues, typeof fieldPaths.firstName) سيكون دائمًا string

من المحتمل أن هذا لن ينجح، بسبب ما تم تعريفه ليقبله setValue.

من الناحية المثالية، كنا سندفع القضية إلى المؤلفين ونأمل في حل سريع، ولكن في غياب ذلك، هناك حلان هنا:

  1. إما استخدام الاسم المستعار UnpackNestedValue والتحويل إليه

    import { UnpackNestedValue, PathValue, Path } from "react-hook-form";
    
    type ValueArgument<f> = UnpackNestedValue<PathValue<f, Path<f>>>;
    
    setValue(fieldPaths.firstName, "David" as ValueArgument<TFormValues>);
    
  2. قم بتمديد الأنواع كلها حتى تقبل القيم الأولية

    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");
      };