typescriptzod

Specify a Zod schema with a non-optional but possibly undefined field


Is it possible to define a Zod schema with a field that is possibly undefined, but non-optional. In TypeScript this is the difference between:

interface IFoo1 {
  somefield: string | undefined;
}

interface IFoo2 {
  somefield?: string | undefined;
}

const schema = z.object({
  somefield: z.union([z.string(), z.undefined()]),
}); // Results in something like IFoo2

As far as I can tell using z.union([z.string(), z.undefined()]) or z.string().optional() results in the field being equivalent to IFoo2.

I'm wondering if there is a way to specify a schema that behaves like IFoo1.

Context / Justification

The reason that I might want to do something like this is to force developers to think about whether or not the field should be undefined. When the field is optional, it can be missed by accident when constructing objects of that type. A concrete example might be something like:

interface IConfig {
  name: string;
  emailPreference: boolean | undefined;
}
enum EmailSetting {
  ALL,
  CORE_ONLY,
}

function internal(config: IConfig) {
  return {
    name: config.name,
    marketingEmail: config.emailPreference ? EmailSetting.ALL : EmailSetting.CORE_ONLY,
  }
}

export function signup(userName: string) {
  post(internal({ name: userName }));
}

This is sort of a contrived example, but this occurs a lot in our codebase with React props. The idea with allowing the value to be undefined but not optional is to force callers to specify that, for example, there was no preference specified vs picking yes or no. In the example I want an error when calling internal because I want the caller to think about the email preference. Ideally the type error here would lead me to realize that I should ask for email preference as a parameter to signup.


Solution

  • You can use the transform function to explicitly set the field you're interested in. It's a bit burdensome, but it works.

    const schema = z
        .object({
            somefield: z.string().optional(),
        })
        .transform((o) => ({ somefield: o.somefield }));
    
    type IFoo1 = z.infer<typeof schema>;
    // is equal to { somefield: string | undefined }