EN VI

Typescript losing generic context on function?

2024-03-13 12:00:07
How to Typescript losing generic context on function

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?

TS Playground

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 =\

Solution:

Replace:

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

with:

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

...and after that, the squggly line should now work on select("").


The reason it doesn't work is because your function from has no way of knowing the database context; what tables are in it and what properties are available within those tables. This means, in fact, that the following code won't error out either:

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

TypeScript won't tell you that the table nonexistent_table does not exist!

Now, by adding annotations like in the solution above, function from will know the shape of the whole database, so it can tell you the valid properties in the table bar.

Answer

Login


Forgot Your Password?

Create Account


Lost your password? Please enter your email address. You will receive a link to create a new password.

Reset Password

Back to login