typescriptgenericsmongoosetypescript-genericstypegoose

TypeScript Generics Multiple Return Types With TypeSafety


I am working on a multitenant app and in that, I use mongoose & typegoose in combination to switch the DB for specific tenants.

Below is a minimal reproducible code that can be seen by installing typegoose and mongoose packages.

import { getModelForClass, mongoose, prop } from '@typegoose/typegoose';
import { AnyParamConstructor, ReturnModelType } from '@typegoose/typegoose/lib/types';

export class DB_ROLES {
  @prop({ type: String })
  name: string;
}

const ROLES_MODEL = getModelForClass(DB_ROLES, {
  schemaOptions: {
    timestamps: true,
    collection: 'roles',
  },
});

export class DB_USERS {
  @prop({ type: String })
  name: string;
}

const DB_USERS_MODEL = getModelForClass(DB_USERS, {
  schemaOptions: {
    collection: 'users',
    timestamps: true,
  },
});

//DATABASE UTILITY
class DatabaseUtil {
  public database: mongoose.Connection;
  public connectDatabase = async (): Promise<boolean> => {
    return new Promise((resolve, reject) => {
      const uri = environment_variables.MONGODB_CONNECTION_STRING ?? '';
      if (this.database) {
        return;
      }
      mongoose.connect(uri, {});
      this.database = mongoose.connection;
      this.database.once('open', async () => {
        console.log('Connected to database');
        resolve(true);
      });
      this.database.on('error', () => {
        console.log('Error connecting to database');
        reject(false);
      });
    });
  };
  getModelForDb<T extends AnyParamConstructor<any>>(databaseName: string, model: ReturnModelType<T>): ReturnModelType<T> & T {
    const db = Mongoose.connection.useDb(databaseName);
    const DbModel = db.model(model.modelName, model.schema) as ReturnModelType<T> & T;
    return DbModel;
  }
  getModelsForDbWithKey<P extends string, T extends AnyParamConstructor<any>, K extends { key: P; model: ReturnModelType<T> }>(
    databaseName: string,
    models: K[]
  ): Partial<Record<P, ReturnModelType<T> & T>> {
    const db = mongoose.connection.useDb(databaseName);
    let result: Partial<Record<P, ReturnModelType<T> & T>> = {};
    let allModels: (ReturnModelType<T> & T)[] = [];
    models.forEach((value) => {
      result[value.key] = db.model(value.model.modelName, value.model.schema) as ReturnModelType<T> & T;
    });
    return result;
  }
}
const DBUtil = new DatabaseUtil();
let singleModel = DBUtil.getModelForDb('base_db', ROLES_MODEL);
singleModel.find({
  //INTELLISENSE WORKS HERE
});
let requiredModels = DBUtil.getModelsForDbWithKey('base_db', [
  { key: 'roles', model: ROLES_MODEL },
  { key: 'users', model: DB_USERS_MODEL },
]);
let roleModel = requiredModels['roles']?.find({
  //INTELLISENSE DOESN'T WORK
});

When I use single Model then I get intellisense as well

enter image description here

Now I am not able to get the typing for the model that I have passed.

enter image description here

This is what I get in return when I hover over the requiredModels object. So is there any way I can get proper typings with Generics. Models can be of different schema so that have different return types.


Solution

  • For your case, you are probably searching for something like a type that can map from a array to a object without losing the types (Like i had asked here after reading this question).

    The resulting code would be:

    // your models before here
    // Extracted the type from the constraint to a interface
    interface ModelListEntry {
      key: string;
      model: ReturnModelType<any>;
    }
    
    // Mapper type that maps a input array of "ModelListEntry" to a Record where the key is from "key" and the value is "model"
    // the key-value information is only kepts if the input array is readonly (const)
    type ModelMapListEntryArrayToRecord<A extends readonly ModelListEntry[]> = {
      // see https://stackoverflow.com/a/73141433/8944059
      [E in Extract<keyof A, `${number}`> as A[E]['key']]: A[E]['model'];
    };
    
    // DATABASE UTILITY
    class DatabaseUtil {
      public database!: mongoose.Connection;
      public connectDatabase = async (): Promise<boolean> => {
        return new Promise((resolve, reject) => {
          const uri = 'mongodb://localhost:27017/';
    
          if (this.database) {
            return;
          }
    
          mongoose.connect(uri, {});
          this.database = mongoose.connection;
          this.database.once('open', async () => {
            console.log('Connected to database');
            resolve(true);
          });
          this.database.on('error', () => {
            console.log('Error connecting to database');
            reject(false);
          });
        });
      };
    
      getModelForDb<T extends AnyParamConstructor<any>>(databaseName: string, model: ReturnModelType<T>): ReturnModelType<T> & T {
        const db = mongoose.connection.useDb(databaseName);
        const DbModel = db.model(model.modelName, model.schema) as ReturnModelType<T> & T;
    
        return DbModel;
      }
    
      getModelsForDbWithKey<List extends readonly ModelListEntry[]>(databaseName: string, models: List): ModelMapListEntryArrayToRecord<List> {
        const db = mongoose.connection.useDb(databaseName);
        const result: Record<string, ModelListEntry['model']> = {};
        for (const { key, model } of models) {
          result[key] = db.model(model.modelName, model.schema);
        }
    
        return result as any;
      }
    }
    
    const DBUtil = new DatabaseUtil();
    const singleModel = DBUtil.getModelForDb('base_db', ROLES_MODEL);
    singleModel.find({
      // INTELLISENSE WORKS HERE
    });
    
    const requiredModels = DBUtil.getModelsForDbWithKey('base_db', [
      { key: 'roles', model: ROLES_MODEL },
      { key: 'users', model: DB_USERS_MODEL },
    ] as const); // change the input array to be read-only, otherwise typescript will just "forget" about the explicit values
    
    requiredModels.roles; // correct type "ReturnModelType<typeof DB_ROLES>"
    requiredModels.users; // correct type "ReturnModelType<typeof DB_USERS>"
    requiredModels.none; // error
    
    const roleModel = requiredModels['roles']?.find({
      // INTELLISENSE WORKS NOW
    });
    

    PS: this answer is thanks to the answer from the mentioned question and its comments.