--- title: Use default() for Optional Fields with Defaults impact: MEDIUM impactDescription: Manual default handling spreads logic across codebase; .default() centralizes defaults in schema tags: refine, default, optional, configuration --- ## Use default() for Optional Fields with Defaults When a field is optional but should have a default value when missing, use `.default()` instead of handling defaults in business logic. This keeps default values centralized in the schema and ensures consistent behavior. **Incorrect (defaults spread across codebase):** ```typescript import { z } from 'zod' const configSchema = z.object({ timeout: z.number().optional(), retries: z.number().optional(), debug: z.boolean().optional(), }) type Config = z.infer function createClient(config: Config) { // Defaults handled in business logic - duplicated everywhere const timeout = config.timeout ?? 5000 const retries = config.retries ?? 3 const debug = config.debug ?? false // ... } function createOtherClient(config: Config) { // Same defaults duplicated - risk of inconsistency const timeout = config.timeout ?? 5000 const retries = config.retries ?? 3 // What if someone uses 2 here? const debug = config.debug ?? false // ... } ``` **Correct (defaults in schema):** ```typescript import { z } from 'zod' const configSchema = z.object({ timeout: z.number().default(5000), retries: z.number().default(3), debug: z.boolean().default(false), }) type Config = z.infer // { timeout: number; retries: number; debug: boolean } // No optional - defaults fill in missing values function createClient(config: Config) { // config.timeout is guaranteed to exist console.log(config.timeout) // 5000 if not provided console.log(config.retries) // 3 if not provided console.log(config.debug) // false if not provided } // Parse fills in defaults configSchema.parse({}) // { timeout: 5000, retries: 3, debug: false } configSchema.parse({ timeout: 10000 }) // { timeout: 10000, retries: 3, debug: false } ``` **Input type vs Output type with defaults:** ```typescript const schema = z.object({ name: z.string(), role: z.enum(['admin', 'user']).default('user'), }) type SchemaInput = z.input // { name: string; role?: 'admin' | 'user' } type SchemaOutput = z.output // { name: string; role: 'admin' | 'user' } // Input type is optional, output type is required ``` **Default with factory function:** ```typescript // Static default const schema1 = z.object({ id: z.string().default('temp-id'), }) // Factory function for dynamic defaults const schema2 = z.object({ id: z.string().default(() => crypto.randomUUID()), createdAt: z.date().default(() => new Date()), }) // Each parse creates new values schema2.parse({}) // { id: 'abc-123...', createdAt: 2024-01-15... } schema2.parse({}) // { id: 'def-456...', createdAt: 2024-01-15... } ``` **Combining with optional/nullable:** ```typescript // .optional().default() - if undefined, use default z.string().optional().default('fallback') // .nullable().default() - null stays null, only undefined gets default z.string().nullable().default('fallback') // null -> null // undefined -> 'fallback' // .nullish().default() - both null and undefined get default z.string().nullish().default('fallback') // null -> 'fallback' // undefined -> 'fallback' ``` **When NOT to use this pattern:** - When absence of value has different meaning than default - When defaults depend on other fields (use transform) Reference: [Zod API - default](https://zod.dev/api#default)