5

I'm working on a similar form to that of my coworker with some extra fields. Ive taken his zodObject and extended it with the extra fields.

He's using a bunch of .refine calls to validate his form, but I wanted to wrap that logic and use it on mine as well.

Whats the best way to extract that logic so we both can use it?

example, take the validations for vehicle and extend for a car object:

export const vehicleZodObject = z.object({
  name: z.string(),
  engine: type: z.enum(['electric', 'combustion']),
})


export const carZodObject = vehicleObject.extend({
  wheels: z.number().min(4),
})

he has a bunch of refine calls chained to his vehicle object like so:

 .refine((data) => isUnique(data.name), {
      message: 'Characters must be unique',
    })

I need to be able to bundle up those refine chains and use them on both zod objects.

Ashbury
  • 2,160
  • 3
  • 27
  • 52

1 Answers1

5

It's a little bit hard to say exactly what the best approach is because I think there are a few options.

First, I would suggest keeping refinements as localized to the field they apply to as possible. For example, instead of calling refine on the entire object, I would call refine on the z.string() for the name field:

export const vehicleZodObject = z.object({
  name: z.string().refine(isUnique, { message: 'Characters must be unique' },
  engine: type: z.enum(['electric', 'combustion']),
});

Next, it's possible to define a helper function which applies refinements to the schema it's handed. You would set it up with an interface that the two schemas share and then call the refine function on the schema handed in:

import { z } from "zod";

interface IShared {
  a: string;
  b: string;
}

function attachRefinements<O extends IShared, T extends z.ZodTypeDef, I>(
  schema: z.ZodType<O, T, I>
) {
  return schema.refine((shared) => shared.a !== shared.b, {
    message: "a and b must be unique"
  });
}

const schema1 = attachRefinements(
  z.object({
    custom: z.boolean(),
    a: z.string(),
    b: z.string()
  })
);

const schema2 = attachRefinements(
  z.object({
    a: z.string(),
    b: z.string(),
    special: z.number()
  })
);

Even in this case, I would suggest creating a base schema that just has fields a and b, adding the refinement just to the base schema and then building schema1 and schema2 using z.intersection. This goes along with the principle that the refinement is kept as close to the place it applies to as possible.

const base = z.object({
  a: z.string(),
  b: z.string(),
}).refine((x) => x.a !== x.b, {
  message: 'a and b must be unique',
});

const schema1 = z.intersection(base, z.object({
  custom: z.boolean(),
});
const schema2 = z.intersection(base, z.object({
  special: z.number()
});
Souperman
  • 5,057
  • 1
  • 14
  • 39