skip to Main Content

I’m working on a form using react-form hooks and using zod for validation. I’m using zod discriminationUnion to dynamically show different fields based on a selection named modes with three options: FCL, LCL, and BULK.

Demo

I have 3 zod schemas for FCL, LCL, and BULK, and they also share common fields like from, to, comment, firstname, lastname, email, and more.

Zod Schema:

// Import the 'z' library, which is used for schema validation.
import * as z from "zod";

// Define a schema for Full Container Load (FCL) mode.
const fclSchema = z.object({
  from: z.object({
    address: z.string(),
    latitude: z.number().min(-90).max(90),
    longitude: z.number().min(-180).max(180),
  }),
  to: z.object({
    address: z.string(),
    latitude: z.number().min(-90).max(90),
    longitude: z.number().min(-180).max(180),
  }),
  modes: z.literal('FCL'), // Specifies the mode as 'FCL'.
  f_container_type: z.enum(["20", "40", "40HC"]), // Specifies container types.
  f_quantity: z.coerce.number().min(1).max(32767),
  f_weight: z.coerce.number(),
  f_unit: z.enum(["kg", "lbs"]),
  comment: z.string().min(3).max(160).optional(), // Optional comment.
  hsn: z.enum(["1", "2", "3"]), // HSN enum.
  date: z.date(), // Date field.
  ico: z.enum(["FCA", "FOB", "CFR", "DDU", "DAP", "DDP"]), // ICO enum.
  insurance: z.boolean(),
  custom: z.boolean(),
  firstName: z.string().min(3).max(18), // First name validation.
  lastName: z.string().min(3).max(18), // Last name validation.
  phone: z.string().min(10).max(15), // Phone number validation.
  email: z.string().email(), // Email validation.
  company: z.string().min(3).max(18), // Company name validation.
});

// Define a schema for Less than Container Load (LCL) mode.
const lclSchema = z.object({
  from: z.object({
    address: z.string(),
    latitude: z.number().min(-90).max(90),
    longitude: z.number().min(-180).max(180),
  }),
  to: z.object({
    address: z.string(),
    latitude: z.number().min(-90).max(90),
    longitude: z.number().min(-180).max(180),
  }),
  modes: z.literal('LCL'), // Specifies the mode as 'LCL'.
  l_container_type: z.enum(["pallete", "boxes", "package", "bag"]), // Container types.
  l_load: z.array(
    z.object({
      l_wid: z.coerce.number().nullish(),
      l_hgt: z.coerce.number().nullish(),
      l_lgt: z.coerce.number().nullish(),
      l_volume_unit: z.enum(["cm", "in"]),
      l_quantity: z.coerce.number().nullish(),
      l_weight: z.coerce.number().nullish(),
      l_weight_unit: z.enum(["kg", "lbs"]),
    })
  ).refine(data => data.every(load => load.l_quantity !== null && load.l_quantity !== undefined && load.l_quantity >= 1), {
    message: 'Quantity must be at least 1',
    path: ['l_load', 'l_quantity'], // Validation for load quantity.
  }),
  comment: z.string().min(3).max(160), // Comment validation.
  hsn: z.enum(["1", "2", "3"]), // HSN enum.
  date: z.date(), // Date field.
  ico: z.enum(["FCA", "FOB", "CFR", "DDU", "DAP", "DDP"]), // ICO enum.
  insurance: z.boolean(),
  custom: z.boolean(),
  firstName: z.string().min(1).max(18), // First name validation.
  lastName: z.string().min(1).max(18), // Last name validation.
  phone: z.string().min(10).max(14), // Phone number validation.
  email: z.string().email(), // Email validation.
  company: z.string().min(1).max(18), // Company name validation.
});

// Define a schema for Bulk mode.
const bulkSchema = z.object({
  from: z.object({
    address: z.string(),
    latitude: z.number().min(-90).max(90),
    longitude: z.number().min(-180).max(180),
  }),
  to: z.object({
    address: z.string(),
    latitude: z.number().min(-90).max(90),
    longitude: z.number().min(-180).max(180),
  }),
  modes: z.literal('BULK'), // Specifies the mode as 'BULK'.
  b_type: z.enum([
    "GC",
    "RC",
    "DG",
    "OOG",
    "BC",
    "C",
    "P/AC",
    "P/CT",
    "P/CC",
    "P/GC",
    "S/HL",
    "S/L",
    "S/R",
    "S/Ro",
    "S/WC",
  ]),
  b_load: z.array(
    z.object({
      b_wid: z.coerce.number().nullish(),
      b_hgt: z.coerce.number().nullish(),
      b_lgt: z.coerce.number().nullish(),
      b_volume_unit: z.enum(["cm", "in"]),
      b_quantity: z.coerce.number().nullish(),
      b_weight: z.coerce.number().nullish(),
      b_weight_unit: z.enum(["kg", "lbs"]),
    })
  ),
  b_loading_rate: z.coerce.number(),
  b_loading_unit: z.enum(["kg/day", "lbs/day"]),
  b_discharge_rate: z.coerce.number(),
  b_discharge_unit: z.enum(["kg/day", "lbs/day"]),
  comment: z.string().min(3).max(160), // Comment validation.
  hsn: z.enum(["1", "2", "3"]), // HSN enum.
  date: z.date(), // Date field.
  ico: z.enum(["FCA", "FOB", "CFR", "DDU", "DAP", "DDP"]), // ICO enum.
  insurance: z.boolean(),
  custom: z.boolean(),
  firstName: z.string().min(1).max(18), // First name validation.
  lastName: z.string().min(1).max(18), // Last name validation.
  phone: z.string().min(10).max(24), // Phone number validation.
  email: z.string().email(), // Email validation.
  company: z.string().min(1).max(18), // Company name validation.
});

// Create a discriminated union schema that selects the appropriate mode schema based on the 'modes' field.
export const profileFormSchema = z.discriminatedUnion("modes", [
  fclSchema,
  lclSchema,
  bulkSchema,
]);

the Problem is all the common elements are showing error messages just fine. However, when it comes to the dynamic element, using {errors.f_quantity && <span className="text-red-500">{errors.f_quantity.message}</span>} is causing an error.

Property 'f_quantity' does not exist on type 'FieldErrors<{ from: { address: string; latitude: number; longitude: number; }; date: Date; custom: boolean; to: { address: string; latitude: number; longitude: number; }; modes: "FCL"; f_container_type: "20" | ... 1 more ... | "40HC"; ... 11 more ...; comment?: string | undefined; } | { ...; } | { ...; }>'.
  Property 'f_quantity' does not exist on type 'Partial<FieldErrorsImpl<{ from: { address: string; latitude: number; longitude: number; }; date: Date; custom: boolean; to: { address: string; latitude: number; longitude: number; }; modes: "LCL"; comment: string; ... 9 more ...; l_load: { ...; }[]; }>> & { ...; }'.ts(2339)

Below code this displays dynamic element when mode===FCL here im using watch hook to monitor modeselection element( const watchMode = watch("modes");)

Code(i have marked error lines below):

  {/* SELECT==FCL */}
              {watchMode === "FCL" && (
                <div>
                  <div className="sm:flex sm:space-x-4 ">
                    <div className="sm:w-1/2 mt-5">
                      <Label className="mb-2">Container type</Label>
                      <Controller
                        name="f_container_type"
                        control={control} 
                        render={({ field }) => (
                          <Select
                            value={field.value}
                            onValueChange={field.onChange}
                          >
                            <SelectTrigger>
                              <SelectValue placeholder="Container type" />
                            </SelectTrigger>
                            <SelectContent>
                              <SelectItem value="20">20' Standard</SelectItem>
                              <SelectItem value="40">40' Standard</SelectItem>
                              <SelectItem value="40HC">
                                40' High cube
                              </SelectItem>
                            </SelectContent>
                          </Select>
                        )}
                      />
                      
                    </div>
                    <div className="sm:w-1/2 mt-5">
                      <Label className="mb-2">Quantity</Label>
                      <Input
                        type="number"
                        placeholder="Enter the Quantity"
                        {...register("f_quantity")}
                      />
//HERE----------------------------------------------->
                     {errors.f_quantity && <span className="text-red-500">{errors.f_quantity.message}</span>}
                    </div>
                  </div>
                  <div className="sm:w-1/2 mt-5">
                    <Label className="mb-2">Weight</Label>

                    <div className="relative max-w-[400px]">
                      <Input
                        type="number"
                        className="py-3 px-4 pr-16 block w-full border-none shadow-sm rounded-md text-sm"
                        placeholder="Enter the Weight"
                        {...register("f_weight")}
                       
                      /> 
//HERE-------------------------------------------------------------------------->
                      {errors.f_weight && <span className="text-red-500">{errors.f_weight.message}</span>}
                      <div className="absolute inset-y-0 right-0 flex items-center  z-20 pr-4">
                        <Controller
                          name="f_unit"
                          control={control} // make sure to define 'control' in your useForm hook
                          defaultValue="kg"
                          render={({ field }) => (
                            <Select
                              value={field.value}
                              onValueChange={field.onChange}
                            >
                              <SelectTrigger className=" border-none text-">
                                <SelectValue />
                              </SelectTrigger>
                              <SelectContent>
                                <SelectItem value="kg">KG</SelectItem>
                                <SelectItem value="lbs">LBS</SelectItem>
                              </SelectContent>
                            </Select>
                          )}
                        />
                      </div>
                    </div>
                  </div>
                </div>
              )}

2

Answers


  1. Chosen as BEST ANSWER

    TypeScript checks the types of variable at compile time only not in runtime.In my case,I have a form with different field(such as f_quantity,f_weight,b_load,l_load...etc) and also same fields as well(such as from,to,comment,firstName...ect).the f_quantity property is only valid when the modes field is FCL. When modes is LCL or BULK, the f_quantity property does not exist according to my schema definitions.

    Solution:
    (errors as any).f_quantity tells TypeScript to treat errors as having any type, which bypasses the type checking for f_quantity. Output:when 0 it shows error message as intended


  2. In typescript, you cannot type-guard with different references. so you have to check mode value on errors object itself.

    if(errors.mode === "FCL") {
        errors.f_quantity // no errors
    }
    

    if you really want to use watch function, you can check both together

    {watchMode === "FCL" && errors.mode === "FCL" && (
       errors.f_quantity // no errors, again :)
    )}
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search