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.

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>
)}
codesandbox
https://codesandbox.io/p/sandbox/github/Amith-AG/discriminatedunion_react_form/tree/master

In typescript, you cannot type-guard with different references. so you have to check
modevalue onerrorsobject itself.if you really want to use
watchfunction, you can check both togetherEdit:
I Checked your code and looks like the errors object does not hold the original value and only holds data about the error.
So, to solve the issue you have, we can create a type-guard function to check if the value is what we want. here is how:
you can see the code at my codesandbox
If you want to know more type guard functions, you can read from typescript documentation