| name | zenstack-nullable-field-types |
| description | Fix TypeScript type errors when passing ZenStack query results to components expecting Zod schema types. Use when: (1) Getting "Type 'null' is not assignable to type 'undefined'" errors, (2) Database query results don't match component prop types, (3) ZenStack findMany/findFirst results have nullable fields but Zod schemas expect optional (undefined) fields, (4) Working with ZenStack + TypeScript in Next.js. Covers null vs undefined handling, type transformations, and proper interface definitions.
|
| author | Claude Code |
| version | 1.0.0 |
| date | "2026-02-03T00:00:00.000Z" |
ZenStack Nullable Field Type Mismatches
Problem
ZenStack query results return null for nullable database fields, but TypeScript interfaces and Zod schemas often expect undefined for optional fields. This causes type errors when passing query results to components or functions that use schema types.
Context / Trigger Conditions
Exact Error Pattern:
error TS2322: Type 'string | null' is not assignable to type 'string | undefined'.
Type 'null' is not assignable to type 'string | undefined'.
When This Occurs:
- Passing ZenStack query results directly to React components
- Component props use Zod schema types (e.g.,
z.infer<typeof mySchema>)
- Database schema has nullable fields:
field String?
- Query results include nullable fields without type transformation
Example Scenario:
const availability = await db.availability.findMany({
select: {
id: true,
reason: true,
},
});
interface Props {
availability: Array<{
id: string;
reason?: string;
}>;
}
<MyComponent availability={availability} />
Solution
Option 1: Define Explicit Database Result Types (Recommended)
Create interfaces that match actual database result types instead of using Zod schema types:
interface AvailabilityFromDB {
id: string;
userId: string;
dayOfWeek: number;
startTime: Date;
endTime: Date;
timezone: string;
reason: string | null;
recurrenceRule: string | null;
effectiveFrom: Date;
effectiveUntil: Date | null;
status: "FREE" | "BUSY";
}
interface Props {
availability: AvailabilityFromDB[];
}
const blocks = await db.availability.findMany();
return <MyComponent availability={blocks} />;
Option 2: Transform Null to Undefined
If you must use Zod schema types, transform the data:
function nullToUndefined<T extends Record<string, any>>(obj: T): T {
const result: any = {};
for (const key in obj) {
result[key] = obj[key] === null ? undefined : obj[key];
}
return result;
}
const blocks = await db.availability.findMany();
const transformed = blocks.map(nullToUndefined);
return <MyComponent availability={transformed} />;
Option 3: Type Assertion (Use Sparingly)
When you're certain the types are compatible:
<MyComponent availability={blocks as ComponentProps['availability']} />
Warning: This bypasses type checking and can hide real issues.
Option 4: Adjust Component Types to Accept Null
Make component props accept null explicitly:
interface Props {
availability: Array<{
id: string;
reason: string | null | undefined;
}>;
}
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
interface Props {
availability: Nullable<AvailabilitySchema>[];
}
Best Practices
1. Use Database Result Types for Server-Fetched Data
type BookingFromDB = {
id: string;
status: string;
learner: { name: string } | null;
};
const bookings: BookingFromDB[] = await db.booking.findMany({
select: {
id: true,
status: true,
learner: { select: { name: true } },
},
});
2. Use Zod Schema Types for Form Data
import { createBookingSchema, type CreateBooking } from "database/schemas";
function BookingForm() {
const [data, setData] = useState<CreateBooking>({
});
}
3. Document the Distinction
type AvailabilityFromDB = {
reason: string | null;
};
type CreateAvailability = z.infer<typeof createAvailabilitySchema>;
ZenStack-Specific Considerations
Nullable Relations
Relations in ZenStack queries return null when not found:
const booking = await db.booking.findFirst({
include: {
learner: true,
},
});
type BookingWithLearner = {
learner: User | null;
};
Optional vs Nullable Fields
ZenStack schema:
model Availability {
reason String? // Optional in schema
}
TypeScript result:
{ reason: string | null }
Select vs Include
Both return nullable types for optional fields:
const result = await db.model.findFirst({
select: { optionalField: true },
});
const result = await db.model.findFirst({
include: { relation: true },
});
Verification
After fixing types:
pnpm tsc --noEmit
Example: Complete Fix
Before (with type errors):
const blocks = await db.availability.findMany({
select: {
id: true,
reason: true,
},
});
interface Props {
blocks: z.infer<typeof availabilitySchema>[];
}
return <AvailabilityList blocks={blocks} />;
After (fixed):
interface AvailabilityBlock {
id: string;
reason: string | null;
}
const blocks: AvailabilityBlock[] = await db.availability.findMany({
select: {
id: true,
reason: true,
},
});
interface Props {
blocks: AvailabilityBlock[];
}
return <AvailabilityList blocks={blocks} />;
Notes
- This issue is common in ORMs (Prisma, ZenStack, TypeORM) because SQL NULL maps to
null in TypeScript
- Zod schemas typically use
optional() which generates undefined, not null
- The mismatch is by design: database layer uses
null, application layer often prefers undefined
- Consider establishing a convention: "DB types use null, business logic uses undefined"
- Some teams create mapper functions to convert between the two
Common Mistakes
- Using Zod Types Everywhere: Don't use
z.infer<> types for database query results
- Ignoring Nullability: Assuming optional fields are
undefined when they're actually null
- Loose Type Assertions: Using
as any to bypass the issue instead of fixing it properly
- Not Handling Relations: Forgetting that relations can be
null even if marked as required in schema
References