我正在研究一个查询构建器,遇到了这样的问题:当构建器在另一个上下文的作用域中作为函数创建时,它会丢失它的泛型类型信息.
构建器负责"构建"一个特定于表的查询,而上下文则跟踪构建器可用的当前状态,从而允许动态添加像投影这样的内容(CTE风格的查询).
上下文被传递给可以materialized 查询的另一个组件(它在幕后存储为AST),从而跟踪从前面更复杂的示例中删除的投影和类型信息.
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?
我的预期是,它推断构建器有一个特定的表绑定到它,然后我可以正确地获取列,因为该对象显然必须使用那些签名绑定到给定表,但只要我try 添加select()
调用,它就不能再处理绑定,这没有意义,因为它should保持相同的Database
类型
编辑:
虽然我可以强制使用上下文对象本身来生成From,但我不明白为什么函数本身不提供绑定,除非我通过from<DummyDatabase, "bar">("bar")...
显式添加它们,但它已经可以检测到from("bar")
本质上等于Builder<DummyDatabase, "bar">
,但SELECT强制它放弃该知识,而这应该根据签名返回等价的Builder<D, T>
……
我使用的是Typescript 5.4
这似乎不可能以这种方式保持整个调用的上下文,因为TS不能沿着链往上看足够远的地方,以使其超过基本的context.with("foo", from("bar"))
.只是说我需要小心任何嵌套的深度,会将显式版本标记为答案,尽管看起来很粗略,如果不传递这些提示=\