- Published on
 
Define a Zod Schema with Non-Optional but Possibly Undefined Fields
- Authors
 - Name
 - Ripal & Zalak
 
Define a Zod Schema with Non-Optional but Possibly Undefined Fields
Problem
In TypeScript, there's a subtle difference between the following interfaces:
interface IFoo1 {
  somefield: string | undefined
}
interface IFoo2 {
  somefield?: string | undefined
}
Here, IFoo1 ensures that the somefield property is always present, but its value can explicitly be undefined. On the other hand, IFoo2 makes somefield optional, which means it may not exist at all.
Zod Schema for Non-Optional Undefined Field
To create a Zod schema resembling IFoo1, you can use z.string().optional() combined with a transformation to ensure the field is always present but may hold undefined. Here's how:
import { z } from 'zod'
const schema = z
  .object({
    somefield: z.string().optional(),
  })
  .transform((obj) => ({ ...obj, somefield: obj.somefield }))
// Type inference
type IFoo1 = z.infer<typeof schema>
// Usage example
const validObject = schema.parse({ somefield: undefined }) // Passes validation
const invalidObject = schema.parse({}) // Fails validation
Common Use Case: Configuration Objects
A practical scenario for this pattern is enforcing strictness in configuration objects:
interface IConfig {
  name: string
  emailPreference: boolean | undefined
}
const configSchema = z
  .object({
    name: z.string(),
    emailPreference: z.boolean().optional(),
  })
  .transform((obj) => ({ ...obj, emailPreference: obj.emailPreference }))
type Config = z.infer<typeof configSchema>
This ensures developers explicitly consider the emailPreference field, preventing accidental omission.
FAQs
1. Why not use null instead of undefined?
While null can represent an intentional "no value," undefined often signifies the absence of initialization. Choosing between them depends on your application's conventions.
2. Can this approach be reused for multiple schemas?
Yes, you can encapsulate this logic in a utility function for reusability:
const nonOptional = <T extends z.ZodTypeAny>(schema: T) =>
  schema.optional().transform((value) => value)
const schema = z.object({
  somefield: nonOptional(z.string()),
})
3. What happens if I don't use .transform?
Without .transform, Zod treats the field as optional, meaning it may not exist in the parsed object, which doesn't align with IFoo1 behavior.
