Replies: 3 comments 2 replies
-
|
The root cause is a TypeScript structural typing issue. When you write: const config = isLocal ? { db: sqliteDb, schema: sqliteSchema } : { db: pgDb, schema: pgSchema }TypeScript infers There are a few ways to fix this, depending on how much code you want to share. Option 1: Initialize once at module level (simplest) Don't create a union — just pick one concrete type at startup: import { drizzle as drizzlePg } from 'drizzle-orm/postgres-js';
import { drizzle as drizzleSqlite } from 'drizzle-orm/better-sqlite3';
const db = isLocal
? drizzleSqlite(sqliteConnection, { schema: sqliteSchema })
: drizzlePg(pgConnection, { schema: pgSchema });
// Then in each module, do the narrowing once:
if (isLocal) {
await (db as ReturnType<typeof drizzleSqlite>).insert(sqliteSchema.table).values({}).returning();
}This works fine at runtime since Drizzle's API surface is consistent across adapters, but you still need to narrow for TypeScript. Option 2: Discriminated union (proper type narrowing) Add a discriminator field so TypeScript can narrow the whole object: type DbConfig =
| { kind: 'sqlite'; db: typeof sqliteDb; table: typeof sqliteSchema.table }
| { kind: 'postgres'; db: typeof pgDb; table: typeof pgSchema.table };
const config: DbConfig = isLocal
? { kind: 'sqlite', db: sqliteDb, table: sqliteSchema.table }
: { kind: 'postgres', db: pgDb, table: pgSchema.table };
// TypeScript narrows both `db` and `table` together here
if (config.kind === 'sqlite') {
await config.db.insert(config.table).values({}).returning();
} else {
await config.db.insert(config.table).values({}).returning();
}Still two branches, but TypeScript is happy and each branch is fully type-safe. Option 3: Repository/service layer (least duplication) If you have many operations to share, abstract the database behind an interface: interface MyRepository {
insertRecord(values: { name: string }): Promise<{ id: number; name: string }[]>;
}
class SqliteRepo implements MyRepository {
constructor(private db: typeof sqliteDb) {}
insertRecord(values: { name: string }) {
return this.db.insert(sqliteSchema.table).values(values).returning();
}
}
class PostgresRepo implements MyRepository {
constructor(private db: typeof pgDb) {}
insertRecord(values: { name: string }) {
return this.db.insert(pgSchema.table).values(values).returning();
}
}
const repo: MyRepository = isLocal ? new SqliteRepo(sqliteDb) : new PostgresRepo(pgDb);
// All callers use the interface — no duplication
await repo.insertRecord({ name: 'test' });The schema-specific code lives in the two repo classes, but all your business logic just calls Which to pick? If you only have a few operations, the discriminated union (Option 2) is the least overhead. If you have many operations across multiple modules, the repository pattern (Option 3) scales better and keeps the SQLite/Postgres details in one place. One more thing: make sure your |
Beta Was this translation helpful? Give feedback.
-
Handling Provider-Specific Return Types (Postgres vs SQLite)I see you're running into the classic "Type Widening" problem when trying to share a single mutation function between SQLite and Postgres. The issue is that The Root Cause: The Solution: Using a Generic Factory FunctionInstead of a static import { PgDatabase } from 'drizzle-orm/pg-core';
import { SQLiteBaseDatabase } from 'drizzle-orm/sqlite-core';
function createMutation<TDB extends PgDatabase<any, any, any> | SQLiteBaseDatabase<any, any, any>>(
db: TDB,
table: any // You can refine this to a generic Table type
) {
return async (values: any) => {
// Drizzle's internal dispatch will handle the correct SQL generation
return await db.insert(table).values(values).returning();
};
}
// Usage:
const runMutation = isLocal
? createMutation(sqliteDb, sqliteSchema.users)
: createMutation(pgDb, pgSchema.users);
await runMutation({ name: 'John' });Best Practice: The "Data Access Object" (DAO) PatternFor complex apps, we recommend defining an interface for your mutations and implementing it twice: interface UserRepo {
create(name: string): Promise<User>;
}
class PgUserRepo implements UserRepo { ... }
class SqliteUserRepo implements UserRepo { ... }
const repo: UserRepo = isLocal ? new SqliteUserRepo() : new PgUserRepo();Why
|
Beta Was this translation helpful? Give feedback.
-
The core issue here is that TypeScript cannot correlate union members across properties. When you write:
```ts
const config = isLocal
? { db: sqliteDb, schema: sqliteSchema }
: { db: pgDb, schema: pgSchema };TypeScript infers {
db: SQLiteDb | PgDb;
schema: SQLiteSchema | PgSchema;
}It loses the relationship between This is a known TypeScript limitation: it does not track correlated unions across object properties. The existing answers cover discriminated unions and the repository pattern well. But there is a more elegant approach that avoids duplicating business logic: a generic factory that captures the correlated types once at initialization. The key insight: lock
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
I've a project that has a distributed (postgres) and standalone (sqlite) mode. What is the recommended approach for using these databases with the same code? The issue mainly comes up on
mutationsFor example
The issue is,
config.schema.tabletype widens and assumes it issqliteSchema.table | pgSchema.tableinstead of the one of the respective schema.Hence,
sqliteSchema.table | pgSchema.tableis always wrong since insert only accepts one and rejects the other.I hope that makes sense.
What is the recommended approach, for using multiple database providers without duplicating code?
Beta Was this translation helpful? Give feedback.
All reactions