我正在研究一个查询构建器,遇到了这样的问题:当构建器在另一个上下文的作用域中作为函数创建时,它会丢失它的泛型类型信息.

构建器负责"构建"一个特定于表的查询,而上下文则跟踪构建器可用的当前状态,从而允许动态添加像投影这样的内容(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?

TS Playground

我的预期是,它推断构建器有一个特定的表绑定到它,然后我可以正确地获取列,因为该对象显然必须使用那些签名绑定到给定表,但只要我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")).只是说我需要小心任何嵌套的深度,会将显式版本标记为答案,尽管看起来很粗略,如果不传递这些提示=\

推荐答案

替换:

const b = new DatabaseContext<DummyDatabase>().with("no", from("bar").select(""))

与:

const b = new DatabaseContext<DummyDatabase>().with("no", from<DummyDatabase, "bar">("bar").select(""))

...在那之后,摇摇晃晃的线现在应该能在select("")号上工作.


它不工作的原因是因为您的函数from无法知道数据库上下文;其中有哪些表以及这些表中有哪些属性.事实上,这意味着下面的代码也不会出错:

new DatabaseContext<DummyDatabase>().with("no", from("nonexistent_table").select(""))

TypeScrip不会告诉你桌子nonexistent_table不存在!

现在,通过添加上面解决方案中的注释,函数from将知道整个数据库的形状,因此它可以告诉您表bar中的有效属性.

Typescript相关问答推荐

如何在不使用变量的情况下静态判断运算式的类型?

当使用`type B = A`时,B的类型显示为A.为什么当`type B = A时显示为`any `|用A代替?

在Angular中注入多个BrowserModules,但在我的代码中只有一个

扩展函数签名中的参数类型,而不使用泛型

如何正确地对类型脚本泛型进行限制

如何键入函数以只接受映射到其他一些特定类型的参数类型?

类型{}上不存在属性&STORE&A;-MobX&A;REACT&A;TYPE脚本

打印脚本中的动态函数重载

如何使我的函数专门针对联合标记?

如何在已知基类的任何子类上使用`extends`?

按函数名的类型安全类型脚本方法包装帮助器

(TypeScript)TypeError:无法读取未定义的属性(正在读取映射)"

使用条件类型的类型保护

基于泛型的条件接口键

回调函数中的TypeScrip类型保护返回Incorect类型保护(具有其他未定义类型的返回保护类型)

为什么TS2339;TS2339;TS2339:类型A上不存在属性a?

可赋值给类型的类型,从不赋值

Typescript 是否可以区分泛型参数 void?

带有过滤键的 Typescript 映射类型

有没有办法从不同长度的元组的联合中提取带有类型的最后一个元素?