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 mode
selection 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
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 asfrom
,to
,comment
,firstName
...ect).thef_quantity
property is only valid when the modes field isFCL
. When modes isLCL
orBULK
, 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.In typescript, you cannot type-guard with different references. so you have to check
mode
value onerrors
object itself.if you really want to use
watch
function, you can check both together