Reputation: 2216
I'm stuck with a generic issue concerning an attempt to write a generic repository with Kysely.
Example: TypeScript Playground
import { Kysely, Selectable } from "kysely";
export type FooTable = {
uuid: string;
name: string;
fromFoo: boolean;
};
export type BarTable = {
uuid: string;
name: string;
fromBar: string;
}
type Table = FooTable | BarTable;
type Tables = {
foo: FooTable;
bar: BarTable;
};
type Data = {
uuid: string;
name: string;
[key: string]: unknown;
}
export interface GenericRepository<T extends Data> {
findOne(id: Data["uuid"]): Promise<T | null>;
/* other methods will follow (like deleteOne, etc) */
}
export function createGenericRepository<
D extends Data,
TableName extends keyof Tables,
T extends Tables[TableName]
>({
dataSource,
tableName,
toModel,
}: {
dataSource: Kysely<Tables>;
tableName: TableName;
toModel: (data: Selectable<T>) => D;
}): GenericRepository<D> {
return {
async findOne(id) {
const data = await dataSource
.selectFrom(tableName)
.selectAll()
.where("uuid", "=", id)
.executeTakeFirst();
if (!data) {
return null;
}
return toModel(data);
}
};
}
function createFooRepository(dataSource: Kysely<Tables>) {
return createGenericRepository({
dataSource,
tableName: "foo",
toModel: (data) => { return { uuid: "123", name: "name", fromFoo: true } }
})
}
My issue is that type checking does not work as I was expected. At line 49 I have
Argument of type 'string' is not assignable to parameter of type 'OperandValueExpressionOrList<Tables, ExtractTableAlias<Tables, TableName>, "uuid">'.(2345).
I don't really understand what I could do to please typescript and kysely here.
Upvotes: 3
Views: 681
Reputation: 311
I've been struggling with this for a couple of hours and I ended up doing something like this with the assumptions that all the tables will have a uuid: string
column:
export interface GenericRepository<T extends Data> {
findOne(id: T extends { id: infer U } ? U extends string ? U : never : never): Promise<T | null>
/* other methods will follow (like deleteOne, etc) */
}
Upvotes: 0
Reputation: 672
This can be achieved using the ref
method from Kysely Dynamic Module
As the documentation says:
This method is meant to be used in those cases where the column names come from the user input or are not otherwise known at compile time.
import { Kysely, Selectable } from "kysely";
export type FooTable = {
uuid: string;
name: string;
fromFoo: boolean;
};
export type BarTable = {
uuid: string;
name: string;
fromBar: string;
}
type Table = FooTable | BarTable;
type Tables = {
foo: FooTable;
bar: BarTable;
};
type Data = {
uuid: string;
name: string;
[key: string]: unknown;
}
export interface GenericRepository<T extends Data> {
findOne(id: Data["uuid"]): Promise<T | null>;
/* other methods will follow (like deleteOne, etc) */
}
export function createGenericRepository<
D extends Data,
TableName extends keyof Tables,
T extends Tables[TableName]
>({
dataSource,
tableName,
toModel,
}: {
dataSource: Kysely<Tables>;
tableName: TableName;
toModel: (data: Selectable<T>) => D;
}): GenericRepository<D> {
return {
async findOne(id) {
const data = await dataSource
.selectFrom(tableName)
.selectAll()
.where(dataSource.dynamic.ref("uuid"), "=", id)
.executeTakeFirst();
if (!data) {
return null;
}
return toModel(data as Selectable<T>);
}
};
}
function createFooRepository(dataSource: Kysely<Tables>) {
return createGenericRepository({
dataSource,
tableName: "foo",
toModel: (data) => { return { uuid: "123", name: "name", fromFoo: true } }
})
}
There's still the need to assert type when calling toModel
callback, and type safety is lost when using dynamic.ref
, so I imagine this solution can still be improved
Upvotes: 0