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);
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.