1👍
Figured it out.
First of all, the reason for the error is because userQuery
was typed as DocumentReference<DocumentData>
but useFirestore<User>(userQuery)
was expecting DocumentReference<User>
. Since those two types did not match the error was shown.
To get around this there are two options:
OPTION 1:
As shown the original question you can coerce the return value of useFirestore()
like so:
const userQuery = doc(db, 'users', 'my-user-id');
const userData = useFirestore(userQuery) as Ref<User>;
While this works, it has some drawbacks:
- It overrides the compiler. I’m basically telling it "I know the return value is something different, but I know better, and I know it exists, so use mine instead". This can cause unforeseen problems down the road.
- If I pass a collection query it needs to be typed as an array (
useFirestore(colQuery) as Ref<User[]>
) and if I pass a document reference it needs to be typed without the array (useFirestore(docRef) as Ref<User>
). Forgetting this can cause other errors. - When using
as
you are saying "this thing exists and it has this type". But sinceuseFirestore
is async sometimes that data does not exist because it is still loading. Therefore it should also have a type ofnull
orundefined
. So actually it would better to useuseFirestore(userQuery) as Ref<User | null | undefined>
which you have to remember to do each time or you may cause issues trying to access some data that doesn’t yet exist.
All this is to say I think it’s better to correctly type the Firestore document reference (or collection reference) and then let useFirestore()
return the correct types without any coercion that could create unforeseen problems.
Which brings us to option 2…
OPTION 2:
In my opinion this is the superior choice.
I discovered in this medium article how to use Firestore withConverter()
to convert data coming to and from Firestore into custom defined objects (i.e. TypeScript interfaces and classes).
If you prefer just to get straight to the code then this GIST has everything you need.
But that solution is for Firebase JavaScript Version 8 SDK. And I am using the Firebase Modular JavaScript Version 9 SDK, which requires some tweaking as shown in my other question here.
In short – withConverter
can be used on the end of Firestore doc
or collection
methods to correctly type them. It takes a single converter
argument. And what we have created below is a re-usable "generic converter" that takes an Interface of your choice.
So you put .withConverter(converter<MyInterfaceHere>())
on the end of collection
or doc
and voila! You have type safety!
Here is the full example:
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);
Now userData
has a type of Ref<User | null | undefined>
and you can access all the User
properties on it without error.
It’s Firestore with all the type safety you love from TypeScript!