drizzle-team / drizzle-team/drizzle-orm
[FEATURE]: Better types
- Dominant language
- TypeScript
- Stars
- 35.8k
- Forks
- 1.6k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 4
Description
### Feature hasn't been suggested before.
- [X] I have verified this feature I'm about to request hasn't been suggested before.
### Describe the enhancement you want to request
### Issue Description
I'm trying to implement a Repository pattern in my application, but I'm running into issues with typing, especially when using the `AnyPgTable` type in Drizzle ORM.
The main problem is that `AnyPgTable` does not allow me to enforce certain field types for the tables. For instance, I want to ensure that a table used with a class has a field like `id` of type `string` and `createdAt` of type `timestamp`. However, since `AnyPgTable` cannot be extended, I have to work around it by using a type intersection like `AnyPgTable & { id: AnyPgColumn, updatedAt: AnyPgColumn }`.
While this seems like a solution, it causes errors in practice. For example, if I try to update a record with a new field, such as `deletedAt`, I get an error because the field isn't recognized by the type system. This issue also extends to using `InferSelectModel` when paired with `InferSelectModel & { id: string }`, as it doesn't work properly due to type mismatches.
### Goal
I am creating a series of classes to help organize the app and would like to leverage some predefined functions that my classes can inherit by extending a base class. Here is an example of how I want the classes to work:
---
### Example Code: Repository Class
```ts
// REPO
import { AnyPgColumn, AnyPgTable } from "drizzle-orm/pg-core";
import { TConnection } from "../database";
import { eq, InferSelectModel } from "drizzle-orm";
import { CustomException } from "../lib/action";
type CustomAnyPgTable = AnyPgTable & {
id: AnyPgColumn;
deletedAt: AnyPgColumn;
};
export abstract class Repo {
abstract table: TTable;
public async find(finder: { id: string }, tx: TConnection) {
const entity = await tx
.select()
.from(this.table)
.where(eq(this.table.id, finder.id));
return entity ? new Entity(entity) : null;
}
public async findOrThrow(
finder: Parameters[0],
tx: TConnection
) {
const user = await this.find(finder, tx);
if (!user) {
throw new CustomException("Not found");
}
return user;
}
public async create(inputs: TTable["$inferInsert"][], tx: TConnection) {
const entities = await tx.insert(this.table).values(inputs).returning();
return entities.map((user) => new Entity(user));
}
public async update(
finder: { id: string },
input: Partial,
tx: TConnection
): Promise {
await tx.update(this.table).set(input).where(eq(this.table.id, finder.id));
}
public async softRemove(
finder: { id: string },
tx: TConnection
): Promise {
await tx
.update(this.table)
.set({
deletedAt: new Date(),
})
.where(eq(this.table.id, finder.id));
}
}
export class Entity<
TTable extends CustomAnyPgTable,
TEntity = InferSelectModel
> {
public data: TEntity;
public constructor(data: TEntity) {
this.data = data;
}
}
```
---
### Problem
In the `softRemove` method of the `Repo` class, the code below doesn't recognize that `deletedAt` is part of the `CustomAnyPgTable` type, and as a result, TypeScript throws an error.
```ts
await tx
.update(this.table)
.set({
deletedAt: new Date(),
})
.where(eq(this.table.id, finder.id));
```
Even though `deletedAt` is defined in the `CustomAnyPgTable` type, TypeScript doesn't infer it correctly in this case, causing issues with the type system.
### Example Usage of the Repository Class
```ts
import { usersTable } from "../database/schema";
import { Repo } from "./utils";
const table = usersTable;
// types
type TTable = typeof table;
export class UserRepo extends Repo {
public table = table;
}
```
---
### Summary of Issues:
1. **Typing issue with `AnyPgTable`**: Can't enforce specific fields (like `id`, `createdAt`) for tables using `AnyPgTable`.
2. **Type intersection workaround**: While using `AnyPgTable & { id: AnyPgColumn, deletedAt: AnyPgColumn }` seems like a solution, it leads to type errors.
3. **`deletedAt` field not recognized**: In the `softRemove` method, TypeScript doesn't recognize `deletedAt` as part of the table schema, causing errors during database updates.
Contributor guide
Assessment
This issue has not been assessed yet.