Effect CommunityEC
Effect Community10mo ago
2 replies
Viridian

Handling `null` and `undefined` values in TypeScript, especially when interfacing with databases,...

Hey all,

I'm using drizzle-orm/sqlite-core with drizzle-orm/libsql to interface with Effect code. I made this table:

export const bookingTable = sqliteTable('bookings', {
    id: text('id').$type<Id>().primaryKey(),
    createdAt: text('created_at').$type<ISO8601DateTime>().notNull(),
    updatedAt: text('updated_at').$type<ISO8601DateTime>().notNull(),
    orgId: text('org_id').$type<Id>().notNull(),
    status: text({ enum: ["PENDING", "APPROVED", "DENIED", "CANCELLED"] }).notNull(),
    contactName: text('contact_name').notNull(),
    contactEmail: text('contact_email').notNull(),
    eventTitle: text('event_title').notNull(),
    eventLocationId: text('event_location_id').$type<Id>().notNull(),
    eventStart: text('event_start').$type<ISO8601DateTime>().notNull(),
    eventEnd: text('event_end').$type<ISO8601DateTime>().notNull(),
    eventDetails: text('event_details').notNull(),
    requestNote: text('request_note')
})

export type StoredBooking = InferSelectModel<typeof bookingTable>


I was working on a way to decode/encode between the stored data and the interface we use to access properties in the code itself;

export const bookingSchema = Schema.Struct({
    id: Id,
    createdAt: ISO8601DateTime,
    updatedAt: ISO8601DateTime,
    orgId: Id,
    status: bookingStatus,
    contact: Schema.Struct({
        name: Schema.String,
        email: Schema.String,
    }),
    event: Schema.Struct({
        title: Schema.String,
        locationId: Id,
        start: ISO8601DateTime,
        end: ISO8601DateTime,
        details: Schema.String
    }),
    requestNote: Schema.optional(Schema.String)
});

export const storedBookingSchema = Schema.Struct({
    id: Id,
    createdAt: ISO8601DateTime,
    updatedAt: ISO8601DateTime,
    orgId: Id,
    status: bookingStatus,
    contactName: Schema.String,
    contactEmail: Schema.String,
    eventTitle: Schema.String,
    eventLocationId: Id,
    eventStart: ISO8601DateTime,
    eventEnd: ISO8601DateTime,
    eventDetails: Schema.String,
    requestNote: Schema.optional(Schema.String),
})

const bookingFromStoredBooking = Schema.transform(
    bookingSchema,
    storedBookingSchema,
    {
      // decode/encode happens here...
    }
)

export const serialise: (b: Booking) => StoredBooking = (b) => pipe(
    Schema.decodeUnknownSync(bookingFromStoredBooking)(b) // ERRORS HERE! Type 'string | undefined' is not assignable to type 'string | null'.
)


Long story short -- it seems that the stored booking type seems to have a type of string | null on requestNote which is an optional field in the sqliteTable. I've read somewhere that Drizzle sets this to null by default instead of being able to return undefined as well. This is causing a discrepancy in the expected types, as Schema defines optional as what is basically T | undefined.

Looking deeper in the StoredBooking type, we see this:

type StoredBooking = {
    id: string & Brand<"Id">;
    createdAt: string & Brand<"ISO8601DateTime">;
    updatedAt: string & Brand<"ISO8601DateTime">;
    orgId: string & Brand<"Id">;
    ... 7 more ...;
    requestNote: string | null;
}


What's the best way to approach this? I think that
undefined
being the default is something I prefer, and I'd love to know if there's an easy way to handle this from the table itself.
Was this page helpful?