Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

zod

typescript

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.

like image 789
Souperman Avatar asked Aug 31 '25 15:08

Souperman


2 Answers

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 }
like image 136
Gus Bus Avatar answered Sep 02 '25 17:09

Gus Bus


Building off the answer from Gus Bus... If you want every property to be required (non-optional), I made a utility function to make this a little easier.

export type Full<T> = { [K in keyof T]-?: [T[K]] } extends infer U
  ? U extends Record<keyof U, [any]>
    ? { [K in keyof U]: U[K][0] }
    : never
  : never;

/** Marks every property as required (non-optional). However property values can still be undefined. */
export function full<T>(x: T) {
  return x as Full<T>;
}

Usage:

const schema = z
  .object({
    name: z.string().optional(),
  })
  .transform(full);

Before (no transform):

enter image description here

After (transform):

enter image description here

Reference: https://stackoverflow.com/a/57334147/704532

like image 43
Casey Plummer Avatar answered Sep 02 '25 15:09

Casey Plummer