Home > Software engineering >  TypeScript Generics Multiple Return Types With TypeSafety
TypeScript Generics Multiple Return Types With TypeSafety

Time:08-01

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.

CodePudding user response:

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.

  • Related