Home > Net >  How do I get a Generic and static reference to a MongoDB collection?
How do I get a Generic and static reference to a MongoDB collection?

Time:04-28

I want to write a static getOne<T>() class method (i.e. it does not require a class instance or the new operator to use it) which is also generic, and which returns MongoDB objects as an Item which is either a Book or a Film. This is my initial idea, but now I don't know how to dynamically use the 'books' or 'films' collection name strings in place of 'xxx'.

import * as mongodb from 'mongodb';

const CONNECTION_STRING = 'MY_CONNECTION_STRING';
const BOOK_COLLECTION_NAME = 'books';
const FILM_COLLECTION_NAME = 'films';

interface Item {
  _id: string
}

interface Book extends Item {
  bookName: string,
  description?: string
}

interface Film extends Item {
  filmTitle: string,
  duration?: number
}

function isBook(item: Item): item is Book {
  return 'bookName' in item;
}

async function getOne<T extends Item>(): Promise<T | null> {

  const client = new mongodb.MongoClient(CONNECTION_STRING);
  await client.connect();
  const db = client.db();

  const collection = db.collection<T>('xxx');
  // Instead of xxx I need to use BOOK_COLLECTION_NAME or FILM_COLLECTION_NAME
  // depending on the actual type of T, how do I do that???
  const item = await collection.findOne({});
  if (item !== null) {
    console.log(item._id);
    if (isBook(item)) {
      // Book type specific implementation
      item.bookName  = ' (best book ever)';
    }
  }
  return item as T;
}

async function test() {
  const book: Book | null = await getOne<Book>();
  console.log(book?.bookName);
  const film: Film | null = await getOne<Film>();
  console.log(film?.filmTitle);
}

test();

Is this even possible? I realise I could workaround this passing it as an argument with something like await getOne<Book>('books'), but I want to prevent that (see the test() function for the desired usage).

CodePudding user response:

This cannot be done with the exact API you want since the type parameters you pass into a function cannot be accessed by the function at runtime (to infer the table name). You can use another approach where the name of the interface is inferred from the collection name so that passing 'books' returns Book | null, passing 'films' returns Film | null, and passing anything else is a compile time error.

type Collections = {
  books: Book,
  films: Film,
}

async function getOne<Name extends keyof Collections>(name: Name): Promise<Collections[Name] | null> {
  // fetch the actual value here
  return {} as Collections[Name];
}
  
async function test() {
  const book: Book | null = await getOne('books');
  console.log(book?.bookName);
  const film: Film | null = await getOne('films');
  console.log(film?.filmTitle);
  // Argument of type '"comments"' is not assignable to parameter of type 'keyof Collections'.
  await getOne('comments');
}

test();

Playground

  • Related