3

When I'm trying to save a snapshot of my query from firestore it returns as q: Query<DocumentData>, and my query snapshot is querySnap: QuerySnapshot<DocumentData>.

DocumentData type is: [field: string]: any; And I can't loop through it without getting any errors.

my effect Code

useEffect(() => {
    const fetchListings = async () => {
      try {
        // Get reference
        const listingsRef = collection(db, "listings");

        // Create a query
        const q = query(
          listingsRef,
          where("type", "==", params.categoryName),
          orderBy("timestamp", "desc"),
          limit(10)
        );

        // Execute query
        const querySnap = await getDocs(q);

        let listings: DocumentData | [] = [];

        querySnap.forEach((doc) => {
          return listings.push({ id: doc.id, data: doc.data() });
        });

        setState((prevState) => ({ ...prevState, listings, loading: false }));
      } catch (error) {
        toast.error("Something Went Wront");
      }
    };

    if (mount.current) {
      fetchListings();
    }

    return () => {
      mount.current = false;
    };
  }, [params.categoryName]);

Does anyone know how to set my listings type correctly?

The listings type from firestore should be:

type GeoLocationType = {
  _lat: number;
  _long: number;
  latitude: number;
  longitude: number;
};

export type ListingsDataType = {
  bathrooms: number;
  bedrooms: number;
  discountedPrice: number;
  furnished: boolean;
  geolocation: GeoLocationType;
  imageUrls: string[];
  location: string;
  name: string;
  offer: boolean;
  parking: boolean;
  regularPrice: number;
  timestamp: { seconds: number; nanoseconds: number };
  type: string;
  userRef: string;
};
Uria Levi
  • 258
  • 1
  • 11
  • 1
    As a side note, `let listings: DocumentData | [] = [];` should be `const listings: DocumentData[] = [];` instead. You should also be making sure that `setState` is not being called after the unsubscribe callback has been invoked (see [this thread](https://stackoverflow.com/a/69140217/3068190) for details) – samthecodingman Jan 26 '22 at 12:47

2 Answers2

7

As you've discovered, you can simply coerce the type of the Reference:

const listingsRef = collection(db, "listings") as CollectionReference<ListingsDataType>;

However, while this works, you may run into future issues where you encounter an unexpected type or you have other nested data that doesn't translate to Firestore very well.

This is where the intended usage of the generic type comes in where you instead apply a FirestoreDataConverter object to the reference to convert between DocumentData and the type of your choosing. This is normally used with class-based types.

import { collection, GeoPoint, Timestamp } from "firebase/firestore";

interface ListingsModel {
  bathrooms: number;
  bedrooms: number;
  discountedPrice: number;
  furnished: boolean;
  geolocation: GeoPoint; // <-- note use of true GeoPoint class
  imageUrls: string[];
  location: string;
  name: string;
  offer: boolean;
  parking: boolean;
  regularPrice: number;
  timestamp: Date; // we can convert the Timestamp to a Date
  type: string;
  userRef: string; // using a converter, you could expand this into an actual DocumentReference if you want
} 

const listingsDataConverter: FirestoreDataConverter<ListingsModel> = {
  // change our model to how it is stored in Firestore
  toFirestore(model) {
    // in this case, we don't need to change anything and can
    // let Firestore handle it.
    const data = { ...model } as DocumentData; // take a shallow mutable copy

    // But for the sake of an example, this is where you would build any
    // values to query against that can't be handled by a Firestore index.
    // Upon being written to the database, you could automatically
    // calculate a `discountPercent` field to query against. (like "what
    // products have a 10% discount or more?")
    if (data.offer) {
      data.discountPercent = Math.round(100 - (model.discountedPrice * 100 / model.regularPrice))) / 100; // % accurate to 2 decimal places
    } else {
      data.discountPercent = 0; // no discount
    }
    return data;
  },

  // change Firestore data to our model - this method will be skipped
  // for non-existant data, so checking if it exists first is not needed
  fromFirestore(snapshot, options) { 
    const data = snapshot.data(options)!; // DocumentData
    // because ListingsModel is not a class, we can mutate data to match it
    // and then tell typescript that it is now to be treated as ListingsModel.
    // You could also specify default values for missing fields here.
    data.timestamp = (data.timestamp as Timestamp).toDate(); // note: JS dates are only precise to milliseconds
    // remove the discountPercent field stored in Firestore that isn't part
    // of the local model
    delete data.discountPercent;
    return data as ListingsModel;
  }
}

const listingsRef = collection(db, "listings")
  .withConverter(listingsDataConverter); // after calling this, the type will now be CollectionReference<ListingsModel>

This is documented in the following places:

samthecodingman
  • 23,122
  • 4
  • 30
  • 54
1

Problem Solved: all I had to do is:

const listingsRef = collection(
          db,
          "listings"
        ) as CollectionReference<ListingsDataType>;
Uria Levi
  • 258
  • 1
  • 11