RobinBlomberg / kysely-codegen

Generate Kysely type definitions from your database.
MIT License
712 stars 67 forks source link

Importing type from generated types #63

Open schwartzmj opened 1 year ago

schwartzmj commented 1 year ago

Let's say I have a table called Examples and the generated types look like this:

import type { ColumnType } from "kysely";

export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
  ? ColumnType<S, I | undefined, U>
  : ColumnType<T, T | undefined, T>;

export interface Examples {
  id: Generated<number>;
  created_at: Generated<Date>;
  updated_at: Generated<Date>;
  text: string;
}

export interface DB {
  examples: Examples;
}

If I'm using something like React or Svelte, and I want to type a prop e.g.:

import type { Examples } from "@db/schema";

type ExampleProps = {
  example: Examples;
};

I'll get errors like the following for the Generated<number> on the id column (in this example, trying to set the value attribute on an input element):

2023-02-18 at 14 44 10

Any way around this? Would it be best to create a file of different queries and infer the type of each result and export those:

const exampleQuery = await db.selectFrom('examples').selectAll().execute();
export type Example = (typeof exampleQuery)[number];

This seems a bit odd, however if I have different queries e.g. one where I selectAll vs selecting certain fields, it seems great to have those typed separately and then I can just import the query from the file as well.

Upvote & Fund

Fund with Polar

RobinBlomberg commented 1 year ago

Interesting problem! I can see the use case.

One possible solution would be to add a helper type to kysely-codegen (or your own codebase) that converts Kysely-specific types into generic types (general idea - I haven't tested this):

export type Model<T extends Record<PropertyKey, unknown>> = {
  [K in keyof T]: T[K] extends ColumnType<infer S, any, any> ? S : T[K];
};

export type ExampleModel = Model<Example>;
// {
//   id: number;
//   created_at: Date;
//   updated_at: Date;
//   text: string;
// };
tgriesser commented 1 year ago

One possible solution would be to add a helper type to kysely-codegen (or your own codebase) that converts Kysely-specific types into generic types (general idea - I haven't tested this):

There's a Selectable helper in Kysely that does just this:

From the example:

import type { Selectable } from 'kysely'
import type { Examples } from "@db/schema";

type ExampleProps = {
  example: Selectable<Examples>;
};

To convert the whole DB:

import { Selectable } from 'kysely'
import type { DB } from './path-to-kyseley.gen'

type DBSelect = {
  [K in keyof DB]: Selectable<DB[K]>
}
RobinBlomberg commented 1 year ago

There's a Selectable helper in Kysely that does just this:

Interesting! Is this a good enough solution for your use case @schwartzmj?

merlindru commented 1 year ago

I'd really appreciate kysely-codegen generating something like

export type Examples = { ... }
export type Example = Selectable<Examples>

This is how I did it when I wrote the types manually -- we won't have to put Selectable<...> everywhere, which gets cumbersome if you need other helper types quickly (imagine Required<Omit<Selectable<Examples>, "foo">>)

jpike88 commented 1 year ago

I wrote this abomination:


type Model<T> = {
    [K in keyof T]: T[K] extends ColumnType<infer S, any, any> ? S : T[K];
};
async function getRow<T extends keyof DB>(
    table: T,
    whereParams: Partial<{
        [s in keyof DB[T]]: Model<DB[T]>[s];
    }>,
    selectors: Array<keyof DB[T]>
): Promise<DB[T]> {
    const row = kysley.selectFrom(table);
    for (const key of Object.keys(whereParams)) {
        row.where(
            key as ReferenceExpression<DB, ExtractTableAlias<DB, T>>,
            '=',
            (whereParams as { [s: string]: any })[key]
        );
    }
    row.select(selectors as SelectExpression<DB, ExtractTableAlias<DB, T>>[]);
    return (await row.executeTakeFirst()) as DB[T];
}

This allowed me to write functions like the below, with a good degree of type safety

const { creditValue } = await sql.getRow(
                    'template',
                    { id: formData.templateID },
                    ['creditValue']
                );

The only thing I'm figuring out is how to get the third argument (which is the selectors I want to use), to properly restrict the properties on the return object. Not quite there yet.

edit: I tried more, but hitting a brick wall. going to stick with the code above for now

type Model<T> = {
    [K in keyof T]: T[K] extends ColumnType<infer S, any, any> ? S : T[K];
};
async function getRow<T extends keyof DB, U extends Array<keyof DB[T]>>(
    table: T,
    whereParams: Partial<{
        [s in keyof DB[T]]: Model<DB[T]>[s];
    }>,
    selectors: U
) {
    const row = kysley.selectFrom(table);
    for (const key of Object.keys(whereParams)) {
        row.where(
            key as ReferenceExpression<DB, ExtractTableAlias<DB, T>>,
            '=',
            (whereParams as { [s: string]: any })[key]
        );
    }
    row.select(selectors as SelectExpression<DB, ExtractTableAlias<DB, T>>[]);
    return (await row.executeTakeFirst()) as { [k in keyof U]: DB[T][k] };
}

edit... improved... sticking with this one instead:


type Model<T> = {
    [K in keyof T]: T[K] extends ColumnType<infer S, any, any> ? S : T[K];
};

async function getRow<T extends keyof DB>(
    table: T,
    whereParams: Partial<{
        [s in keyof DB[T]]: Model<DB[T]>[s];
    }>,
    selectors?: Array<keyof DB[T]>
): Promise<Model<DB[T]>> {
    const row = kysley.selectFrom(table);
    for (const key of Object.keys(whereParams)) {
        row.where(
            key as ReferenceExpression<DB, ExtractTableAlias<DB, T>>,
            '=',
            (whereParams as { [s: string]: any })[key]
        );
    }
    row.select(selectors as SelectExpression<DB, ExtractTableAlias<DB, T>>[]);
    return (await row.executeTakeFirst()) as Model<DB[T]>;
}
koskimas commented 4 months ago

It would be awesome + in-line with the intended use of Kysely if kysely-codegen exported the selectable insertable and updateable types. Something like this:

export interface UserTable {
  id: Generated<string>
  ...
}

export type User = Selectable<UserTable>
export type NewUser = Insertable<UserTable>
export type UserUpdate = Updateable<UserTable>

This is how we instruct people to structure their types in the docs.

sjunepark commented 4 months ago

+1