I am working on a query builder and running into issues where the builder loses it's generic type information when created as a function within the scope of another context.
The builder is responsible for "building" a table specific query, while the context is tracking the current state available to the builders, allowing for things like projections to be added in dynamically (CTE style queries).
Context gets passed to another component that can materialize the query (it's stored as AST under the covers) hence tracking the projections and type information that was removed from earlier more complicated example.
type Database = { tables: Record<string, Record<string, any>> }
interface DummyDatabase {
tables: {
"foo": { id: number, name: string },
"bar": { name: string, "type": number }
}
}
class Builder<D extends Database, T extends keyof D["tables"], C extends keyof D["tables"][T] = keyof D["tables"][T]> {
columns?: C[]
table: T
constructor(table: T, columns?: C[]){ this.table = table; this.columns = columns}
select(...columns: C[]): Builder<D, T> {
return new Builder(this.table, columns)
}
}
class DatabaseContext<D extends Database> {
from<T extends keyof D["tables"]>(table: T): Builder<D, T> {
return new Builder(table)
}
with<A extends string, T extends keyof D["tables"], R extends Record<string, any>>(alias: A, builder: Builder<D, T>): DatabaseContext<{ tables: { [key in keyof D["tables"]]: D["tables"][key] } & Record<A, R> }> {
// Track alias and builders that provide for future use, that's why params are here above, leaving out for brevity...
return new DatabaseContext()
}
}
function from<D extends Database, T extends keyof D["tables"]>(table: T): Builder<D, T> {
return new Builder(table)
}
const a = new DatabaseContext<DummyDatabase>().from("foo").select("id")
const b = new DatabaseContext<DummyDatabase>().with("no", from("bar").select()) // <-- Lost intellisense...why?
My expectation was that the fact that it inferred that the builder had a specific table binding to it, that I could then get the columns correctly since that object obviously has to be bound to the given table with those signatures, but as soon as I try to add the select()
call it can no longer handle the bindings which doesn't make sense since it should keep the same Database
type
Edit:
While I can force usage of the context object itself to generate the from, I don't understand why the function itself doesn't providing the bindings unless I explicitly add them via from<DummyDatabase, "bar">("bar")...
but it already can detect that from("bar")
is inherently equal to Builder<DummyDatabase, "bar">
but the select forces it to drop that knowledge when that should be returning an equivalent Builder<D, T>
based on the signature...
I'm using typescript 5.4
Seems that this isn't possible to keep the context across the calls this way as TS can't look far enough back up the chain to get it beyond just the basic context.with("foo", from("bar"))
. Just means I need to be careful with any nested depth, will mark the explicit version as the answer though it seems rough that I can't get it without having to pass those hints =\