TypeScript中函数内泛型上下文丢失问题求助
TypeScript查询构建器中独立from函数调用链丢失泛型类型信息的问题
我在开发一款查询构建器,用于构建特定表的查询,同时通过DatabaseContext跟踪查询状态、支持动态添加投影(类似CTE风格)。但遇到一个关键问题:当在DatabaseContext.with()方法中使用独立的from()函数并链式调用select()时,TypeScript会丢失泛型类型信息,导致列的智能提示完全失效。
简化复现代码
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> }> { // 省略跟踪别名和构建器的逻辑 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") // 异常:select()丢失列的类型提示 const b = new DatabaseContext<DummyDatabase>().with("no", from("bar").select())
问题原因分析
- 独立from函数的泛型无上下文约束:独立的
from()函数泛型参数D没有绑定到当前DatabaseContext的DummyDatabase类型,TypeScript会将D推断为最宽泛的Database类型,而非具体的DummyDatabase。 - 类型推断的正向性限制:TypeScript的类型推断是正向推导,无法从
with()方法对builder的类型要求(必须是Builder<DummyDatabase, "bar">)反向回溯,约束from("bar")的泛型参数。它只能处理context.with("foo", from("bar"))这种无链式调用的基础场景。 - select()的类型依赖:
select()的参数类型依赖于Builder的D和T泛型,当D被推断为宽泛的Database时,自然无法识别bar表的具体列。
解决方案
方案1:显式指定泛型参数(直接但不够便捷)
手动给from()指定泛型参数,强制绑定到DummyDatabase:
const b = new DatabaseContext<DummyDatabase>().with("no", from<DummyDatabase, "bar">("bar").select("name"));
方案2:创建绑定上下文的from工厂函数
修改DatabaseContext,添加一个返回绑定当前数据库类型的from函数的方法,让TypeScript自动继承上下文的泛型信息:
class DatabaseContext<D extends Database> { // ... 原有代码 ... createFrom() { return <T extends keyof D["tables"]>(table: T): Builder<D, T> => { return new Builder(table); }; } } // 使用示例 const ctx = new DatabaseContext<DummyDatabase>(); const boundFrom = ctx.createFrom(); // 此时select()有完整的列提示 const b = ctx.with("no", boundFrom("bar").select("name"));
方案3:调整with()方法的泛型定义
重新设计with()的泛型参数,让它从传入的builder中提取数据库类型,同时保证与当前上下文兼容:
class DatabaseContext<D extends Database> { // ... 原有代码 ... with<A extends string, BD extends D, BT extends keyof BD["tables"], R extends Record<string, any>>( alias: A, builder: Builder<BD, BT> ): DatabaseContext<{ tables: D["tables"] & Record<A, R> }> { return new DatabaseContext(); } }
这种方式让TypeScript能从builder中推断出具体的BD类型,同时通过BD extends D保证与当前上下文的数据库类型兼容。
内容的提问来源于stack exchange,提问作者Nathan Northcutt
相关产品推荐
相关产品推荐

