Is there a way to organize my database connections with object literals in TypeScript?

Viewed 87

I want to write two different connections in my code: one is for production and the other is for development. I also want to choose between these connections with a .env file. I tried several ways, and I found this solution:

// Modules
import dotenv from "dotenv";
import knex from "knex";

// Environment variables
dotenv.config();
const { PG_HOST, PG_PORT, PG_USER, PG_PASSWORD, PG_DB, IS_APP_IN_PRODUCTION } = process.env;

// Connections
const connectionsList = [
    knex({
        client: "pg",
            connection: {
                host: PG_HOST,
                port: (PG_PORT as any),
                user: PG_USER,
                password: PG_PASSWORD,
                database: PG_DB
            }
    }),

    knex({
        client: "sqlite3",
        connection: {
            filename: "./development/dev.sqlite3",
            database: "dev-db"
        }
    })
];

// Setting the connection
const connection = connectionsList[IS_APP_IN_PRODUCTION == "true" ? 0 : 1];

// Export
export default connection;

This works and solves my problem, but in my point of view, this isn't the best solution. I don't like the idea of using an array to organize my connections; because of that, my first try was to use object literals to organize, in this way:

// Modules
import dotenv from "dotenv";
import knex from "knex";

// Environment variables
dotenv.config();
const { PG_HOST, PG_PORT, PG_USER, PG_PASSWORD, PG_DB, APP_MODE } = process.env;

// Connections
const connectionsList = {
    production: knex({
        client: "pg",
            connection: {
                host: PG_HOST,
                port: (PG_PORT as any),
                user: PG_USER,
                password: PG_PASSWORD,
                database: PG_DB
            }
    }),

    development: knex({
        client: "sqlite3",
        connection: {
            filename: "./development/dev.sqlite3",
            database: "dev-db"
        }
    })
};

// Setting the connection
const connection = connectionsList[APP_MODE as string]; // APP_MODE is a string that can be "production" or "development"

// Export
export default connection;

But doing this way gives me this error:

The element implicitly has a type 'any' because the expression of type 'string' cannot be used for the index type '{ production: Knex<any, unknown[]>; development: Knex<any, unknown[]>; }'. No index signature with a parameter of type 'string' was found in type '{ production: Knex<any, unknown[]>; development: Knex<any, unknown[]>; }'.

Is there a way to solve this problem? If yes, how? And if no, how should I write my code?

1 Answers

The problem is that the compiler only knows that APP_MODE is a string (or actually string | undefined), which is not enough for the compiler to be sure that it is a key of connectionsList. For all the compiler knows, APP_MODE === "testing", and then you're looking up connectionsList.testing which doesn't exist.

You can either explicitly test APP_MODE, after which the compiler will be happy:

if (APP_MODE !== "production" && APP_MODE !== "development") 
  throw new Error("Uh oh, bad APP_MODE");

const connection = connectionsList[APP_MODE]; // okay

Or, you can just assert that APP_MODE is one of those two values instead of string:

const connection = connectionsList[APP_MODE as "development" | "production"]; // okay

Explicit testing is safer than asserting (since the former catches edge cases and the latter does not), but both ways let the compiler know that APP_MODE can be treated as a key of connectionsList.

Playground link to code

Related