AMunim
AMunim

Reputation: 1171

[Easy Generics]Generic Extended Type for zod in child component typescript

I have a base interface for a form, but each of my pages extends that to create an extended form. Now I want to create a child component that generates a form based on the extended template.

minimal reproducible

// childComponent.tsx  
export default function ProductSourceFormTemplate<T extends IProductSourceBase>({ formSchema }: {
    formSchema: z.ZodType<T>,
}) {  
    type FormValues = z.infer<typeof formSchema>
    const defaultValues: T = useMemo<T>(() => ({
        sourceId: ""
    }) as T, [searchParams])
    const form = useForm<FormValues>({
        resolver: zodResolver(formSchema),
        defaultValues: defaultValues,
    });

    // render form
}

but it gives this error on defaultValues

Type 'T' is not assignable to type 'AsyncDefaultValues<T> | DefaultValues<T> | undefined'.
  Type 'IProductSourceBase' is not assignable to type 'AsyncDefaultValues<T> | DefaultValues<T> | undefined'.
    Type 'IProductSourceBase' is not assignable to type 'DefaultValues<T>'.ts(2322)

and also none of the names are recognised when I want to create form elements.

What I want is for my parent component to create productBaseSchema.extend({}) which is a z.ZodObject<{}> and then pass this extended schema to the child const extendedSchema: z.ZodObject<z.objectUtil.extendShape<{}> that can also give onSubmit, render functions in its props so only parent only deals with extra fields.

Examples:


export interface IProductSourceBase {
    productId: string;
    sourceId: number;
    sourceMeta?: any;
}

const productSourceBaseSchema = z.object({
    productId: z.string().nonempty(translate("Product ID is required")),
    sourceId: z.coerce.number().int(translate("Source ID must be an integer")).gt(0, translate("A source must be selected")),
});

const extendedSchema = productSourceBaseSchema.extend({
    sourceMeta: z.object({
        sourceUrl: z.string().url(translate("This is not a valid URL")).nonempty(translate("Url is required")).refine(
            (url) => /\/spreadsheets\/d\/([a-zA-Z0-9-_]{16,44})/.test(url), // Test for valid Spreadsheet ID
            {
                message: translate("This is not a valid Google Spreadsheet ID"), // Custom error message
            }
        ),
        cell: z.string().nonempty(translate("Cell number is required")).refine(
            (cell) => /^[A-Za-z]+[1-9]\d*$/.test(cell), // Check if the cell matches the pattern
            {
                message: translate("Invalid cell reference format"), // Custom error message for invalid format
            }
        ),
    }),
});

Upvotes: 0

Views: 48

Answers (1)

Nicholas Dullam
Nicholas Dullam

Reputation: 529

Here, I'd ensure you're using FormValues as the assignment for both defaultValues and useForm, while also using react-hook-form's generic for DefaultValues -- this applies a deep partial to the given type (just to clean things up). And, for forms like this, if you're ever incorporating a transform or default into your zod schema, using z.input rather than z.infer can make things a bit easier, but doesn't directly apply here.

import { DefaultValues, ... } from "react-hook-form"

...
    type FormValues = z.infer<typeof formSchema>
    const defaultValues = useMemo<DefaultValues<FormValues>>(() => ({
      sourceId: ""
    }), [])
    
    const form = useForm<FormValues>({
      resolver: zodResolver(formSchema),
      defaultValues,
    });
...

Upvotes: 0

Related Questions