1

I'm trying to implement a lazy database connection collection in typescript. I'm creating a class called DatabaseCollection and my idea is to use a proxy to lazy load the connections (I'm using knex as connector) the class works fine in terms of node context, but Typescript says "property does not exists" and I don't know how to tell to Typescript about a dynamic properties class.

I use a workaround converting to Record<string, Knex> but I want to implement it properly, thanks in advance.

Here is the code:

const debug = true
process.env['DB_TEST_URL'] = 'mysql://root@localhost/test';

class NotImportantConnector {
    public url: string;

    constructor(url: string) {
        this.url = url;
    }
}

function dbFromURL(url: string): NotImportantConnector {
    return new NotImportantConnector(url);
}

class DatabasesCollection<T> {
    protected databases: Record<string, T> = {};

    constructor() {
        return new Proxy(this, this) as any;
    }

    get (target: DatabasesCollection<T>, name: string): T {
        if (name in target.databases) {
            return target.databases[name];
        }

        const envName = `DB_${name.replace(/[A-Z]/g, l => `_${l}`).toUpperCase()}_URL`;

        if (!process.env[envName]) {
            throw new Error(`Call to database ${name} needs ${envName} environment variable to run`);
        }

        target.databases[name] = dbFromURL(process.env[envName] as string) as T;
        return target.databases[name];
    }
}


// Not working with error Property 'test' does not exist on type 'DatabasesCollection<NotImportantConnector>'.
// const db = new DatabasesCollection<NotImportantConnector>();

// Workaround, using as 
const db = new DatabasesCollection<NotImportantConnector>() as unknown as Record<string, NotImportantConnector>;

console.log(db.test);
6
  • 1
    Please consider providing a self-contained minimal reproducible example that we can copy and paste into our own IDEs to see what you're talking about without unrelated errors. TypeScript by itself doesn't have process (that's node specific, right?) or dbFromURL or Knex in scope. It would be helpful if you'd either define/import them (and if importing, tag the question with appropriate dependencies), or even better: replace them with native things. Anything that makes it easier for others to get immediately to work on the issue will make it more likely you get a useful answer. Commented May 23, 2023 at 17:11
  • @jcalz I understand what you saying, but the question is not about run, the app works wth my workaround, indeed you can use other types instead the knex connection. Commented May 23, 2023 at 18:44
  • I am not talking about runtime either; I'm talking about being able to copy and paste that code into a standalone TypeScript IDE and reproduce the typing issue you're talking about. If you edit the code here to be a minimal reproducible example then I'll take another look; otherwise we are at an impasse and I'll disengage to cut down on noise so as not to distract others. Good luck! Commented May 23, 2023 at 18:48
  • @jcalz Now are minimal reproducible example, hope now you can rest comftably tonight XD Commented May 23, 2023 at 21:17
  • Does this approach meet your needs? If so I'll write up an answer explaining; if not, what am I missing? Commented May 23, 2023 at 21:35

1 Answer 1

1

TypeScript isn't going to make it easy to use a class declaration to make DatabasesCollection<T> behave the way you want, which is that every instance should presumably have all the known properties of the class, plus an index signature where every other property key has a value of type T. But TypeScript can't directly represent this "every other property" concept; there's a longstanding open feature request for this at microsoft/TypeScript#17867, but it's not part of the language yet. So adding an index signature to the class directly won't behave exactly as desired. See How to define Typescript type as a dictionary of strings but with one numeric "id" property for various alternative approaches in general.

For your use case, it would be acceptable to make instances of DatabasesCollection<T> be the intersection of the known instance type with {[k: string]: T} (aka Record<string, T>). This behaves well enough when accessing a value of that type... although it's hard to actually produce a value of that type in a way the compiler sees as type safe.

And that means we'll need to use something like a type assertion to convince the compiler that your class constructor produces instances of that type.

To do this, I'd suggest renaming your DatabasesCollection<T> class out of the way to, say, _DatabasesCollection<T> so we can use the name DatabasesCollection for the name of the desired instance type (_DatabasesCollection<T> & Record<string, T>), and the name of the asserted class constructor. Like this:

class _DatabasesCollection<T> {
  ⋯ // same impl, more or less
}

type DatabasesCollection<T> = _DatabasesCollection<T> & Record<string, T>;
const DatabasesCollection = _DatabasesCollection as new <T>() => DatabasesCollection<T>;

Let's test it out:

const db = new DatabasesCollection<NotImportantConnector>();
// const db: DatabasesCollection<NotImportantConnector>

const t = db.test;
// const t: NotImportantConnector

Looks good.

Playground link to code

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.