undefined for Optional Fields Instead of null✅ Accepted - 2025-01-26
When integrating Zod schemas (domain-first) with Drizzle ORM (database layer), we encountered type mismatches between optional fields. The core issue was the difference between:
undefined for absent optional propertiesNULL for missing valuesThis manifested as type errors requiring as any casts when inserting data:
// ❌ Before: Required type casting
const [createdReservation] = await db
.insert(reservations)
.values(reservationData as any) // Type mismatch!
.returning();
With exactOptionalPropertyTypes: true in TypeScript config:
email?: string | undefinedemail?: string or email: string | nullundefined → null manuallyexactOptionalPropertyTypesundefined ✅We chose to align the Drizzle schema with our domain-first Zod schemas by using undefined for all optional fields.
undefined as NULL automaticallyUpdated all optional Drizzle schema fields to use undefined:
// ✅ After: Aligned with domain model
export const reservations = pgTable('reservations', {
// Required fields
date: timestamp('date').notNull(),
customer: jsonb('customer').$type<CustomerInfo>().notNull(),
// Optional fields use undefined (not null)
tableIds: jsonb('table_ids').$type<string[] | undefined>(),
notes: text('notes').$type<string | undefined>(),
tags: jsonb('tags').$type<string[] | undefined>(),
source: reservationSourceEnum('source').$type<'phone' | 'web' | 'walk_in' | 'email' | 'other' | undefined>(),
confirmedAt: timestamp('confirmed_at').$type<Date | undefined>(),
// ... other optional timestamp fields
});
Imported exact Zod types to ensure perfect alignment:
import type { CustomerInfo } from '@seatkit/types';
Routes now work with full type safety and zero manual conversions:
// ✅ Clean, type-safe code
const reservationData = {
...request.body,
date: new Date(request.body.date),
status: request.body.status || 'pending',
};
const [createdReservation] = await db
.insert(reservations)
.values(reservationData) // No casting needed!
.returning();
as any type castsexactOptionalPropertyTypes: true$type<T>() annotations for all optional fieldsnull)$type() APIexactOptionalPropertyTypes: Documentation$type() method: Custom types documentation/ARCHITECTURE.md - Domain-first approachWhen adding new optional fields, follow this pattern:
// ❌ Don't: Default Drizzle (uses null)
newField: text('new_field'),
// ✅ Do: Align with domain (uses undefined)
newField: text('new_field').$type<string | undefined>(),
This ensures consistency with our domain-first architecture and maintains type safety.