I'm using firebase to interact with firstore and am taking advantage of the "withConverter" method to handle the mapping of data to and from my domain objects. I want to try and eliminate having to write custom withConverter logic for all my domain objects but keep getting typescript errors when trying to create a generic version. I'm not sure it's even possible.
I want to do something like
export function collectionWithConverter<T,D>(TCreator:{new ():T},ref: CollectionReference<DocumentData>): CollectionReference<T> {
return ref.withConverter({
toFirestore: (obj: T) => {
return obj.toFirestoreJSON()
},
fromFirestore: (snapshot: QueryDocumentSnapshot, options: SnapshotOptions) => {
const data = snapshot.data(options) as D
return new TCreator({...data, "id": snapshot.id})
}})
}
But I get errors: 'data' is declared but its value is never read.ts(6133) Type 'CollectionReference' is not assignable to type 'CollectionReference'. Type 'DocumentData' is not assignable to type 'T'. 'T' could be instantiated with an arbitrary type which could be unrelated to 'DocumentData'
export enum ItemCollectionFields {
ID = "id",
ITEM_ID = "itemID",
NAME = "name",
DESCRIPTION = "descriptionText",
}
const ICF = ItemCollectionFields
export interface ItemProps {
[ICF.ID]: string,
[ICF.ITEM_ID]: string,
[ICF.NAME]: string,
[ICF.DESCRIPTION]: string|null,
}
export class Item {
id: string|null = null
name: string|null = null
descriptionText: string|null = ""
constructor(props:Partial<ItemProps>) {
this[ICF.ID] = props[ICF.ID]
this[ICF.NAME] = props[ICF.NAME]
this[ICF.DESCRIPTION] = props[ICF.DESCRIPTION]
}
toJSON(): ItemProps {
return {
[ICF.ID]: this[ICF.ID],
[ICF.NAME]: this[ICF.NAME],
[ICF.DESCRIPTION]: this[ICF.DESCRIPTION],
}
}
toFirestoreJSON(): Partial<ItemProps> {
const {id, ...rest} = this.toJSON()
for (const key in rest) {
if (rest[key] === "" || rest[key] === null || rest[key] === undefined) {
delete rest[key]
}
}
return rest
}
static defaultItem():Item {
return new Item({id: "", name: "", descriptionText: ""})
}
}
------------------------------------------------------------------------------------
export function getCollectionReference<T>(collectionName:Collections, converter: (ref: CollectionReference<DocumentData>)=>CollectionReference<T>): CollectionReference<T> {
return converter(firestoreAccessor(collectionName))
}
export function getDocumentReference<T>(collectionName:Collections, itemID:string, converter: (ref: DocumentReference<DocumentData>)=>DocumentReference<T>): DocumentReference<T> {
return converter(doc(getFirestore(), collectionName, itemID))
}
export function collectionWithConverter(ref: CollectionReference<DocumentData>): CollectionReference<Item> {
return ref.withConverter({
toFirestore: (Item: Item) => {
return Item.toFirestoreJSON()
},
fromFirestore: (snapshot: QueryDocumentSnapshot, options: SnapshotOptions) => {
const data = snapshot.data(options) as ItemProps
return new Item({...data, "id": snapshot.id, "itemID": snapshot.id})
}})
}
export function documentWithConverter(ref: DocumentReference<DocumentData>): DocumentReference<Item> {
return ref.withConverter({
toFirestore: (Item: Item) => {
return Item.toFirestoreJSON()
},
fromFirestore: (snapshot: QueryDocumentSnapshot, options: SnapshotOptions) => {
const data = snapshot.data(options) as ItemProps
return new Item({...data, "id": snapshot.id, "itemID": snapshot.id})
}})
}
------------------------------------------------------------------------------------
#Example Use case
export async function getItem(itemID: string): Promise<Item|null> {
const ref = getDocumentReference<Item>(Collections.ITEMS, itemID, documentWithConverter)
const snap = await getDoc(ref)
if (snap.exists) {
return snap.data()
}
return null
}