K
Kysely•15mo ago
Kristian Notari

Subquery type error when using generics

I have the following simple query being generated by my function f:
const f = () => queryBuilder
.deleteFrom(`table as t1`)
.whereExists(qb => qb.selectFrom(`table as t2`)
.whereRef('t1.col1', '>=', 't2.col1')
)
const f = () => queryBuilder
.deleteFrom(`table as t1`)
.whereExists(qb => qb.selectFrom(`table as t2`)
.whereRef('t1.col1', '>=', 't2.col1')
)
and it works fine. If I add a generic for reusing the same function with different tables (all sharing the common col1 column) it works fine, until I get to the sub query whereRef (even if I constraint the generic to a single value, which is the same table as before):
const f = <T extends 'table'>(table: T) => queryBuilder
.deleteFrom(`${table} as t1`)
.whereExists(qb => qb.selectFrom(`${table} as t2`)
/* here it can't find the refs to t1.col1 or t2.col1 */.whereRef('t1.col1', '>=', 't2.col1')
)
const f = <T extends 'table'>(table: T) => queryBuilder
.deleteFrom(`${table} as t1`)
.whereExists(qb => qb.selectFrom(`${table} as t2`)
/* here it can't find the refs to t1.col1 or t2.col1 */.whereRef('t1.col1', '>=', 't2.col1')
)
6 Replies
Kristian Notari
Kristian Notari•15mo ago
Argument of type 'string' is not assignable to parameter of type 'ReferenceExpression<From<From<DB, `${T} as old`>, "${T} as new">, FromTables<From<DB, `${T} as old`>, ExtractAliasFromTableExpression<DB, `${T} as old`>, "${T} as new">>'.ts(2345)
Argument of type 'string' is not assignable to parameter of type 'ReferenceExpression<From<From<DB, `${T} as old`>, "${T} as new">, FromTables<From<DB, `${T} as old`>, ExtractAliasFromTableExpression<DB, `${T} as old`>, "${T} as new">>'.ts(2345)
I found out that if you spread the T over an union type as the generic of the first method of the query builders (eg. deletedFrom and selectFrom), then everything works as expected:
const f = <T extends 'table1' | 'table2'>(table: T) => queryBuilder
.deleteFrom<'table1 as t1' | 'table2 as t1'>(`${table} as t1`)
.whereExists(qb => qb.selectFrom<'table1 as t2' | 'table2 as t2>(`${table} as t2`)
/* here it can't find the refs to t1.col1 or t2.col1 */.whereRef('t1.col1', '>=', 't2.col1')
)
const f = <T extends 'table1' | 'table2'>(table: T) => queryBuilder
.deleteFrom<'table1 as t1' | 'table2 as t1'>(`${table} as t1`)
.whereExists(qb => qb.selectFrom<'table1 as t2' | 'table2 as t2>(`${table} as t2`)
/* here it can't find the refs to t1.col1 or t2.col1 */.whereRef('t1.col1', '>=', 't2.col1')
)
This works And you can easily construct such generic types for first methods of query builder as:
type GenericAliasFrom<T extends string, A extends string> = `${T} as ${A}`
type GenericAliasFrom<T extends string, A extends string> = `${T} as ${A}`
where T should be 'table1' | 'table2' and not the generic T type inside the function itself, otherwise it would not work
Igal
Igal•15mo ago
Hey 👋 Is this resolved? What is the reasoning behind the helper/s here? Seems like a case of premature / overly complex DRY application.
Kristian Notari
Kristian Notari•15mo ago
As I said, I need to do the same exact query for multiple tables that share a set of columns (maybe I want to update the updated_at column, or set the "deleted_at" column, etc). The problem being, if kysely builder first method get its generic from some generic T it can't successfully hint (and accept) any string references then inside where conditions and such. By explicitly setting the first type parameter to the deleteFrom or selectFrom and so on, as the distributed union when aliasing such generic T you then get correct type inference for the rest of the query builder methods. This surely happens when you're aliasing a generic table type, so as
`${T} as t1`
`${T} as t1`
for example As long as it concerns me, I found out how to overcome this type inference problem by explicitly annotating the query builder method as I showed before. Not sure if this is something worth investigating further in order to make the type annotation unnecessary. It's still type safe, even when annotating it explicitly, I'm just redistributing the template literal type, nothing fancy there
koskimas
koskimas•15mo ago
Seems like something that's very difficult to get working with kysely. And also something you maybe shouldn't try to do with it. You can either have super strict types, or super generic types. Not both unfortunately
Kristian Notari
Kristian Notari•15mo ago
Yeah I mean, it's all about not being too generic. Instantiating the right type helping it by using explicit annotation is doable and type safe still so, why not?
koskimas
koskimas•15mo ago
Often these become unreadable, unmaintainable and impossible for other people to understand even if you get them working. At some point those things out weight type-safety