drizzle-team / drizzle-team/drizzle-orm

[FEATURE]: Better types

Open
#3,673 6 comments 3 reactions 0 assignees View on GitHub
enhancement
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.