Search code examples
reactjstypescriptreact-hook-formzodshadcnui

Type assignment error in array of object: Typescript, Dynamic Forms using Shadcn UI, React hook form and ZOD


I am trying to accept two fields [{fileName: string, file: File}] in a React-based Typescript Dynamic form, where the user can add multiple sets of fields [{fileName: string, file: File}] on successful ZOD validation. Below are the codes:

Type:

interface MultipleFileType {
    info: [{
        fileName: string;
        file: File
    }]
}

Validation Schema

const multipleSchema = z.object({
    fileName: z.string({ required_error: "Name is required" }).min(5, { message: "Minimum length is 5" }),
    file: z
        .any()
        .refine((files) => files?.length == 1, 'Image is required.')
        .refine((files) => files?.[0]?.size <= (1024 * 1024 * 2.5), `Max file size is 2.5 MB.`)
        .refine(
            (files) => ['image/jpeg', 'image/jpg', 'image/png'].includes(files?.[0]?.type),
            '.jpg, .jpeg, .png and .webp files are accepted.',
        ),
})

export const multipleFileUploaderSchema = z.object({
    info: z.array(
        multipleSchema
    )
})

Form:

<Form {...form}>
 <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
  {fields.map((item, index) => (
   <div key={item.id}>
    <div className="md:flex md:flex-col">
     <FormField
      key={index}
      control={form.control}
      name={`info.${index}.fileName`} <------- ERROR 1
      render={({ field }) => (
       <FormItem>
        <FormLabel>Filename</FormLabel>
        <FormControl>
         <Input type="text" {...field} />
        </FormControl>
        <FormMessage />
       </FormItem>
      )}
     />
    </div>
    <div className="md:flex md:flex-col">
     <FormField
      key={index}
      control={form.control}
      name={`info.${index}.file`} <------- ERROR 2
      render={({ field: { onChange, ...fieldProps } }) => (
       <FormItem>
        <FormLabel>Picture</FormLabel>
        <FormControl>
         <input
          type="file"
          {...fieldProps}
          value={""}
          onChange={(event) => {
           const { files } = getImageData(event, index);
           // setPreviewUrl(displayUrl);
           onChange(files);
          }}
          ref={imgInputRef}
          hidden
         />
        </FormControl>
        {/* <FormMessage /> */}
       </FormItem>
      )}
     />
    </div>
    <Button
     disabled={fields.length === 1}
     className="text-red-500"
     variant="outline"
     size="icon"
     type="button"
     onClick={() => remove(index)}
    >
     <Trash className="h-4 w-4" />
    </Button>
    <Button
     disabled={fields.length > 2.5 * 1024 * 1024 || !form.formState.isValid}
     className="ml-4"
     variant="outline"
     size="icon"
     type="button"
     onClick={() =>
      append({
       fileName: "",
       file: new File([], "/vite.svg"),
      })
     }
    >
     <Plus className="h-4 w-4" />
    </Button>
   </div>
  ))}
  <Button type="submit" className="ml-4">
   Submit
  </Button>
 </form>
</Form>;

The ERROR occurred in the both the markings ERROR 1 and ERROR 2 is as follows: Type 'info.${number}.fileName' is not assignable to type '"info" | "info.0" | "info.0.fileName" | "info.0.file"'.ts(2322) controller.d.ts(20, 5): The expected type comes from property 'name' which is declared here on type 'IntrinsicAttributes & { render: ({ field, fieldState, formState, }: { field: ControllerRenderProps<MultipleFileType, "info" | "info.0" | "info.0.fileName" | "info.0.file">; fieldState: ControllerFieldState; formState: UseFormStateReturn<...>; }) => ReactElement<...>; } & UseControllerProps<...>'

Any suggestions would help.


Solution

  • You can always cast those name properties to a Path<z.infer<typeof yourSchemaHere>> type, where Path is exported from react-hook-form, although the way your types are structured you're telling typescript that the array will always have a length of 1, and with the code that you shared it's hard to tell if that is the same index that you're mapping over. If you do not want that array to always have a length of 1, change this:

    interface MultipleFileType {
        info: [{
            fileName: string;
            file: File
        }]
    }
    export const multipleFileUploaderSchema = z.object({
        info: z.array(
            multipleSchema
        )
    })
    

    to this:

    interface MultipleFileType {
        info: {
            fileName: string;
            file: File
        }[]
    }
    export const multipleFileUploaderSchema = z.object({
        info: multipleSchema.array()
    })