Finally figured it out!
To understand the root cause of the error, it's important to note that userQuery
was mistakenly declared as
DocumentReference<DocumentData>
, whereas
useFirestore<User>(userQuery)
expected a type of
DocumentReference<User>
. This mismatch led to the display of the error message.
In order to resolve this issue, there are two viable options:
Plan A:
You can implement a type coercion on the returned value of useFirestore()
like demonstrated in the original question:
const userQuery = doc(db, 'users', 'my-user-id');
const userData = useFirestore(userQuery) as Ref<User>;
While this approach may work, it comes with some limitations:
- It bypasses the compiler and assumes ownership of the return value, potentially leading to future complications.
- If passing a collection query, it needs to be typed as an array (
useFirestore(colQuery) as Ref<User[]>
), while for a document reference, it should be typed without the array (useFirestore(docRef) as Ref<User>
). Failure to adhere to these rules can result in additional errors.
- By using
as
, you assert that the data exists and has a specific type. However, since useFirestore
functions asynchronously, the data might not yet be available during loading. Therefore, it would be better to specify the type accordingly by incorporating null
or undefined
. This oversight should be kept in mind each time to prevent issues related to accessing non-existent data prematurely.
Ultimately, it is recommended to accurately define the Firestore document reference (or collection reference) types and allow useFirestore()
to naturally return the correct types without engaging in coercion that could introduce unforeseen challenges.
This leads us to option B...
Option B:
I firmly believe that this alternative is more advantageous.
Through my discovery in this insightful Medium article, I learned about leveraging Firestore withConverter()
to facilitate the conversion of data flowing to and from Firestore into custom-defined objects (i.e., TypeScript interfaces and classes).
If you prefer a direct approach to the code, you can refer to this GIST containing all necessary details.
However, please note that this solution pertains to Firebase JavaScript Version 8 SDK, whereas my implementation involves the Firebase Modular JavaScript Version 9 SDK, necessitating some modifications as outlined in my related inquiry here.
In essence, withConverter
can be appended after Firestore doc
or collection
methods to establish precise typing. It accepts a single converter
parameter. Below, we have devised a versatile "generic converter" accommodating an Interface of your choosing.
Simply affix
.withConverter(converter<MyInterfaceHere>())
at the end of
collection
or
doc
, and voilà! You now possess type safety!
The complete example is illustrated below:
interface User {
id?: string;
name: string;
}
const converter = <T>() => ({
toFirestore: (data: PartialWithFieldValue<T>) => data,
fromFirestore: (snap: QueryDocumentSnapshot) => snap.data() as T,
});
const userDocRef = doc(db, 'users', 'my-user-id').withConverter(
converter<User>()
);
const userData = useFirestore(userDocRef);
Subsequently, userData
corresponds to
Ref<User | null | undefined></code, enabling seamless access to all <code>User
attributes without any hitches.
Embrace Firestore with the robust type safety features inherent to TypeScript!