Issue
I am using this useFirestore() wrapper provided by VueUse.org
But when typing useFirestore()
with a generic I cannot use an interface that has optional properties because TypeScript throws this error:
No overload matches this call
In the examples below the User
interface has a required name
property which causes typescript to throw the error.
interface User {
id?: string;
name: string; // <-- When this property is required the error will show, changing it to optional removes the error
}
const userQuery = doc(db, 'users', 'my-user-id');
const userData = useFirestore<User>(userQuery); // <-- The error shows here on "userQuery"
I have found one workaround, but it's not great.
You can use useFirestore(userQuery) as Ref<User>
which removes the error and gives it the correct type.
But doing this is not a good solution because I'm basically telling the compiler "I know better" than what the compiler is saying. So it would be much better to have proper typing without resorting to this workaround.
interface User {
id?: string;
name: string;
}
const userQuery = doc(db, 'users', 'my-user-id');
const userData = useFirestore(userQuery) as Ref<User>; // <-- See changes made here
Is there a better way of doing this?
Solution
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!
Answered By - TinyTiger
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.