Reputation: 3466
Here is some code in charge of validating a form schema on our front-end application.
We are having trouble with Zod library accessing a discriminatedUnion
specific branch's property from within refine()
validation callback. The endDate
property is absent from auto-suggest completion, the property is being underlined in red by Typescript language server saying "Property 'endDate' does not exist on type …".
export const formSchema = z.object({
// Few fields here ...
contract: z.object({
startDate: z.string().describe('text'),
contractType: z.enum(['III', 'DDD']).describe('select'),
// Few other fields here ...
specificationContrat: z.discriminatedUnion('contractType', [
z.object({
contractType: z.literal('III'),
...someSchema.shape,
}),
z.object({
contractType: z.literal('DDD'),
endDate: z.string().describe('text'),
...someSchema.shape,
})
]),
})
.refine(
(contract) => {
return contract.contractType === 'III' ||
(contract.contractType === 'DDD' && new Date(contract.specificationContrat.endDate) > new Date(contract.startDate));
// "Property 'endDate' does not exist on type …"______________________________^
},
'"endDate" has to be anterior to "startDate"'
)
});
Upvotes: 0
Views: 126
Reputation: 1564
Actually, if I'm narrowing a type, I can then define the type with z.infer
and simplify the refine function:
const IIIContractSchema = z.object({
contractType: z.literal("III"),
// ... other properties
});
const DDDContractSchema = z.object({
contractType: z.literal("DDD"),
endDate: z.string().describe("text"),
// ... other properties
});
const specificationContratSchema = z.discriminatedUnion("contractType", [
IIIContractSchema,
DDDContractSchema,
]);
// Infer TypeScript types from Zod schemas
type IIIContract = z.infer<typeof IIIContractSchema>;
type DDDContract = z.infer<typeof DDDContractSchema>;
type SpecificationContrat = z.infer<typeof specificationContratSchema>;
// Now define your form schema
export const formSchema = z.object({
contract: z
.object({
startDate: z.string().describe("text"),
contractType: z.enum(["III", "DDD"]).describe("select"),
specificationContrat: specificationContratSchema,
})
.refine(
(contract): boolean => {
if (contract.contractType === "III") {
return true;
} else if (contract.contractType === "DDD") {
const spec = contract.specificationContrat as DDDContract;
return new Date(spec.endDate) > new Date(contract.startDate);
}
return false;
},
{ message: '"endDate" must be after "startDate"' }
),
});
Upvotes: 1
Reputation: 1564
I can only think of two ways, both of them are clumsy and related to type narrowing
to help typescript deal with inference and definitions:
Narrow it inline:
.refine(
(contract) => {
if (contract.contractType === "DDD") {
const spec = contract.specificationContrat;
if (spec.contractType === "DDD") {
// TypeScript now knows 'spec' is of type '{ contractType: "DDD"; endDate: string }'
return new Date(spec.endDate) > new Date(contract.startDate);
}
}
// For other contract types or if no validation is needed
return true;
},
{ message: '"endDate" has to be after "startDate"' }
),
or create a type guard:
.refine(
(contract) => {
// Type guard to check if contract.specificationContrat is of type 'DDD'
const isDDDContract = (
spec: typeof contract.specificationContrat
): spec is { contractType: "DDD"; endDate: string } =>
spec.contractType === "DDD";
return (
contract.contractType === "III" ||
(isDDDContract(contract.specificationContrat) &&
new Date(contract.specificationContrat.endDate) >
new Date(contract.startDate))
);
},
{ message: '"endDate" has to be after "startDate"' }
),
Upvotes: 1