I'm trying to create a strictly typed API that only allows valid inputs. The API uses firestore internally, which admits any JSON document as a valid input, and I want to strictly limit what are valid inputs, both in the document path and the document structure. I don't want to create a gigantic file with all the types and have to update it every-time I add a new type that should be taken into account. I want to have the types and the valid document types collocated where they are used. For that reason, I'm trying to use module augmentation and extend an interface that says which values are acceptable and in which combinations. The objective is to associate certain object shapes with certain collection names and only admit valid values for those but also only valid combinations of them.
This is the generic wrapper around firebase that sets the basic stuff:
import { collection, DocumentReference, addDoc as addFirebaseDoc } from 'firebase/firestore';
import { getFirestoreDb } from '~/getFirestoreDb';
export interface DocsList {}
export async function addDoc<
Type extends keyof DocsList,
Collection extends DocsList[Type][0],
Data extends DocsList[Type][1],
>(collectionName: Collection, doc: Data): Promise<DocumentReference> {
const db = collection(getFirestoreDb(), collectionName);
const result = await addFirebaseDoc(db, doc);
return result;
}
When there was just one extension, it worked as expected:
// diary.ts file
declare module './database' {
interface DocsList {
readonly Diary: [Collections.diary, DiaryInput];
}
}
addDoc('random-string', validDocument) // fails as expected
addDoc(Collectiobs.diary, invalidDocument) // fails as expected
Also, if I check the type of addDoc I can see it is correctly inferring the type of document and limiting to it as you can see on this screenshot:
However, when I added to more extensions to the interface the inference is not correct. The problem is that instead of limiting the document type based on the provided collection it infers that the expected document is a sum type of all the possible valid documents, and that is not what I want.
For example, given the following extension:
declare module './database' {
interface DocsList {
readonly Log: [Collections.logs, LogEntryInput];
}
}
This should not be permitted, but it is:
addDoc(Collections.logs, diaryEntry); // should fail, but pass
addDoc(Collections.diary, logEntry); // should fail, but pass
I also tried the following generics, to see if it is possible to limit it to the inferred value of type, but it does not work either:
export async function addDoc<
Type extends keyof DocsList,
Info extends DocsList[Type],
Collection extends Info[0],
Data extends Info[1],
>(collectionName: Collection, doc: Data): Promise<DocumentReference> {
const db = collection(getFirestoreDb(), collectionName);
const result = await addFirebaseDoc(db, doc);
return result;
}
In case anyone wants to play with it, I created a little sandbox example:
https://codesandbox.io/s/module-augmentation-problem-1fuw2v?file=/src/index.ts