You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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())

问题原因分析

  1. 独立from函数的泛型无上下文约束:独立的from()函数泛型参数D没有绑定到当前DatabaseContext的DummyDatabase类型,TypeScript会将D推断为最宽泛的Database类型,而非具体的DummyDatabase。
  2. 类型推断的正向性限制:TypeScript的类型推断是正向推导,无法从with()方法对builder的类型要求(必须是Builder<DummyDatabase, "bar">)反向回溯,约束from("bar")的泛型参数。它只能处理context.with("foo", from("bar"))这种无链式调用的基础场景。
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 04:58:10