Firebase Generic withConverter Typescript


Jag använder Firebase för att interagera med Firestore och utnyttjar metoden "withConverter" för att hantera mappningen av data till och från mina domänobjekt. Jag vill försöka eliminera behovet av att skriva egen withConverter-logik för alla mina domänobjekt men får hela tiden TypeScript-fel när jag försöker skapa en generisk version. Jag är inte säker på om det ens är möjligt.

Jag vill göra något liknande

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})
    }})
}

Men jag får fel: 'data' är deklarerat men dess värde läses aldrig.ts(6133) Typen 'CollectionReference' kan inte tilldelas typen 'CollectionReference'. Typen 'DocumentData' kan inte tilldelas typen 'T'. 'T' kan instansieras med en godtycklig typ som kan vara orelaterad till '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
}

Svar #1

Exempel på att konvertera en databas typ till en lokal typ (i detta exempel, lägga till id från databasen till den lokala typen):

interface DatabaseType {
  foo: boolean;
}
interface LocalType extends DatabaseType {
  id: string;
}

const converter: FirestoreDataConverter<LocalType> = {
    toFirestore: (item) => item,
    fromFirestore: (snapshot: QueryDocumentSnapshot<DatabaseType>, options) => {
      const data = snapshot.data(options);
      return {
        ...data,
        id: snapshot.id
      };
    }
};

const q = query(...).withConverter(converter);

Svar #2

Det verkar som att det är möjligt att skriva ett generiskt FirestoreDataConverter-objekt enligt denna guide. Det anpassade objektet i guiden är skrivet för att ta från vilken typ som helst, precis som du för närvarande försöker göra. Jag har testat specifikt den generiska datakonverteraren:

// Test class
class User{
    constructor(readonly fName: string, readonly lName: string){
        this.fName = fName;
        this.lName = lName;
    }

    toString(): string{
        return `First name: ${this.fName}, Last name: ${this.lName}`;
    }

}

//Generic converter
const genericConverter = <T>() => ({
    toFirestore(data: Partial<T>) {
        return data;
    },
    fromFirestore(snapshot: FirebaseFirestore.QueryDocumentSnapshot): T {
        return snapshot.data() as T;
    }
});

//Sample function to get document
async function getFireDocs(){
    const userSnap = await fireDB
        .collection('simpleUser')
        .withConverter(genericConverter<User>())
        .doc('simpleUser1').get();
    const testUser = new User(userSnap.data().firstName, 
userSnap.data().lastName);
    if (testUser !== undefined) {
        console.log(testUser.fName);
        console.log(testUser.lName);
        console.log(testUser.toString());
        console.log(testUser)
    }
}
getFireDocs();

Utdata :

John
Smith
First name: John, Last name: Smith
User { fName: 'John', lName: 'Smith' }

Den kompletta koden från den här artikeln finns här. Det verkar som att användningen av en Partial-typ hjälper i det här fallet, eftersom det tillåter att egenskaper är valfria i objektmodeller. Här är den fullständiga FirestoreDataConveter-dokumentationen med ytterligare exempel.

Relaterade artiklar