kysely-org / kysely

A type-safe typescript SQL query builder
https://kysely.dev
MIT License
10.74k stars 271 forks source link

Writing generic insertion function #496

Closed dhatch-niv closed 1 year ago

dhatch-niv commented 1 year ago

Hi,

Thanks for your work on Kysely. I have a question about creating a generic shared function that returns generated columns from the table. I want to use this to build a class that contains basic CRUD functionality for my app. I've tried a number of ways and am hoping you have some deeper knowledge of the Typescript typing incantation necessary that I do.

Goal

I'd like to create a function that inserts an object and returns a generated column (in this example id, but in my real use case id and createdAt -- I know there is already built in functionality for an autogenerated id column).

My first try was this:

async function createAndReturnId<DB, TableName extends keyof DB & string>(
  db: Kysely<DB>,
  table: TableName,
  object: Insertable<DB[TableName]>
): Promise<{ id: string }> {
  const result = await db
    .insertInto(table)
    .values(object)
    // Error: Argument of type '"id"[]' is not assignable to parameter of type 'SelectArg<DB, TableName, SelectExpression<DB, TableName>>'.
    .returning(["id"]) 
    .executeTakeFirstOrThrow();
  return result;
}

Kysely doesn't know that the id column can be selected from DB[TableName]. The full error is:

Argument of type '"id"[]' is not assignable to parameter of type 'SelectArg<DB, TableName, SelectExpression<DB, TableName>>'.
  Type '"id"[]' is not assignable to type 'readonly SelectExpression<DB, TableName>[]'.
    Type 'string' is not assignable to type 'SelectExpression<DB, TableName>'.ts(2345)

Attempts

If I constrain the DB to require that every table has an id field, I can write the function:

async function createAndReturnIdAll<
  DB extends { [K in keyof DB]: { id: string } },
  TableName extends keyof DB & string
>(
  db: Kysely<DB>,
  table: TableName,
  object: Insertable<DB[TableName]>
): Promise<{ id: string }> {
  const result = await db
    .insertInto(table)
    .values(object)
    .returning(["id"])
    .executeTakeFirstOrThrow();
  return result;
}

This compiles without any errors.

However, this is over-constraining DB. I'd like to only require that the TableName table has the ID field. I'm not sure how to write this type constraint. I tried below:

async function createAndReturnIdJustTN<
  DB extends { [K in keyof DB]: K extends TableName ? { id: string } : DB[K] },
  TableName extends keyof DB & string
>(
  db: Kysely<DB>,
  table: TableName,
  object: Insertable<DB[TableName]>
): Promise<{ id: string }> {
  const result = await db
    .insertInto(table)
    .values(object)
    .returning(["id"]) // Error
    .executeTakeFirstOrThrow();
  return result;
}

It seems that this doesn't help the Typescript compiler at all. There is still the same error as before.

Is there a better way to write the constraint on DB such that the compiler is able to understand the SelectExpression type appropriately for the returning builder method?

Workaround

Currently I am using the db.dynamic feature with some type casing as a workaround for this.

koskimas commented 1 year ago

You either get very strict types of generic types. Both at the same time is often impossible. Kysely has very strict types.