I am writing an Angular application which uses Firestore. I want to be able to use my interfaces and FirestoreDataConverters (which are declared in an Angular Library) in my Firebase Functions too. The main issue is that that the QueryDocumentSnapshot class has slightly different implementations in firebase-admin/firestore and @firebase/firestore.
I'm trying to come up with a solution based on this comment on a GitHub Issue.
So far I have declared a new QueryDocumentSnapshot by doing:
import { QueryDocumentSnapshot as QueryDocumentSnapshotFire } from '@angular/fire/firestore';
import { QueryDocumentSnapshot as QueryDocumentSnapshotAdmin } from '@google-cloud/firestore';
export type QueryDocumentSnapshot = QueryDocumentSnapshotAdmin | QueryDocumentSnapshotFire;
then, I have declared my types and converter as follows:
export type FirestoreUser = z.infer<typeof firestoreUserSchema>;
export const firestoreUserSchema = z.object({
email: z.string(),
});
export type FirestoreUserDb = z.infer<typeof firestoreUserDbSchema>;
export const firestoreUserDbSchema = z.object({
email: z.string(),
});
export const firestoreUserConverter: FirestoreDataConverter<FirestoreUser, FirestoreUserDb> = {
fromFirestore: (snapshot: QueryDocumentSnapshot): FirestoreUser => {
const validFirestoreUserDb = firestoreUserDbSchema.parse(snapshot.data());
return {
email: validFirestoreUserDb.email,
};
},
toFirestore: (firestoreUser: FirestoreUser): FirestoreUserDb => {
const validFirestoreUser = firestoreUserSchema.parse(firestoreUser);
return {
email: validFirestoreUser.email,
};
},
};
now, I am able to use my firestoreUserConverter on the frontend:
const userDocRef = doc(this.firestore, FirestoreCollections.Users, uid).withConverter(firestoreUserConverter);
where the type of userDocRef is correctly inferred as
const userDocRef: DocumentReference<{
email: string;
}, {
email: string;
}>
but, in my Firebase Functions, when I try to do something like
const result = await db.collection(FirestoreCollections.Users).doc(user.uid).withConverter(firestoreUserConverter).set({
email: null, // This should only accept string
});
I do not get an error saying "Cannot assign null to string" because the .set method does not infer any types from the converter. I am 100% sure that this is possible because during my experiments I briefly achieved the aforementioned error but have since changed things up and have no idea what went wrong.
The problem is that the DocumentReference
returned from withConverter
is not typed correctly when using firebase-admin unless I type it explicitly:
const result = await db.collection(FirestoreCollections.Users).withConverter<FirestoreUser, FirestoreUserDb>(firestoreUserConverter).doc(user.uid).set({
email: user.email,
});
when typed explicitly, the returned DocumentReference
is of type
FirebaseFirestore.DocumentReference<{
email: string;
}, {
email: string;
}>
but if I do not type it, it's just
FirebaseFirestore.DocumentReference<unknown, FirebaseFirestore.DocumentData>
while on the frontend the type is correctly inferred from the converter function even when I don't type the withConverter
function explicitly.
My questions are:
- Why is the type not inferred correctly when calling set()?
- Is there a better way to achieve sharing FirestoreDataConverters between my FE and BE implementations? I don't really like importing @google-cloud/firestore in my library.
- Should I change approach completely and use something like Typesaurus instead?
I am writing an Angular application which uses Firestore. I want to be able to use my interfaces and FirestoreDataConverters (which are declared in an Angular Library) in my Firebase Functions too. The main issue is that that the QueryDocumentSnapshot class has slightly different implementations in firebase-admin/firestore and @firebase/firestore.
I'm trying to come up with a solution based on this comment on a GitHub Issue.
So far I have declared a new QueryDocumentSnapshot by doing:
import { QueryDocumentSnapshot as QueryDocumentSnapshotFire } from '@angular/fire/firestore';
import { QueryDocumentSnapshot as QueryDocumentSnapshotAdmin } from '@google-cloud/firestore';
export type QueryDocumentSnapshot = QueryDocumentSnapshotAdmin | QueryDocumentSnapshotFire;
then, I have declared my types and converter as follows:
export type FirestoreUser = z.infer<typeof firestoreUserSchema>;
export const firestoreUserSchema = z.object({
email: z.string(),
});
export type FirestoreUserDb = z.infer<typeof firestoreUserDbSchema>;
export const firestoreUserDbSchema = z.object({
email: z.string(),
});
export const firestoreUserConverter: FirestoreDataConverter<FirestoreUser, FirestoreUserDb> = {
fromFirestore: (snapshot: QueryDocumentSnapshot): FirestoreUser => {
const validFirestoreUserDb = firestoreUserDbSchema.parse(snapshot.data());
return {
email: validFirestoreUserDb.email,
};
},
toFirestore: (firestoreUser: FirestoreUser): FirestoreUserDb => {
const validFirestoreUser = firestoreUserSchema.parse(firestoreUser);
return {
email: validFirestoreUser.email,
};
},
};
now, I am able to use my firestoreUserConverter on the frontend:
const userDocRef = doc(this.firestore, FirestoreCollections.Users, uid).withConverter(firestoreUserConverter);
where the type of userDocRef is correctly inferred as
const userDocRef: DocumentReference<{
email: string;
}, {
email: string;
}>
but, in my Firebase Functions, when I try to do something like
const result = await db.collection(FirestoreCollections.Users).doc(user.uid).withConverter(firestoreUserConverter).set({
email: null, // This should only accept string
});
I do not get an error saying "Cannot assign null to string" because the .set method does not infer any types from the converter. I am 100% sure that this is possible because during my experiments I briefly achieved the aforementioned error but have since changed things up and have no idea what went wrong.
The problem is that the DocumentReference
returned from withConverter
is not typed correctly when using firebase-admin unless I type it explicitly:
const result = await db.collection(FirestoreCollections.Users).withConverter<FirestoreUser, FirestoreUserDb>(firestoreUserConverter).doc(user.uid).set({
email: user.email,
});
when typed explicitly, the returned DocumentReference
is of type
FirebaseFirestore.DocumentReference<{
email: string;
}, {
email: string;
}>
but if I do not type it, it's just
FirebaseFirestore.DocumentReference<unknown, FirebaseFirestore.DocumentData>
while on the frontend the type is correctly inferred from the converter function even when I don't type the withConverter
function explicitly.
My questions are:
- Why is the type not inferred correctly when calling set()?
- Is there a better way to achieve sharing FirestoreDataConverters between my FE and BE implementations? I don't really like importing @google-cloud/firestore in my library.
- Should I change approach completely and use something like Typesaurus instead?
1 Answer
Reset to default 0Here are my thoughts regarding your issue:
- The first thing I noticed is that you didn’t import zod for schema validation and data conversion. If you try to run code without importing zod, TypeScript won't recognize methods like
z.object()
orz.string()
because those are part of the zod library.
Updated code:
import { QueryDocumentSnapshot as QueryDocumentSnapshotFire } from '@angular/fire/firestore';
import { QueryDocumentSnapshot as QueryDocumentSnapshotAdmin } from '@google-cloud/firestore';
import { z } from 'zod'; // Import zod
export type QueryDocumentSnapshot = QueryDocumentSnapshotAdmin | QueryDocumentSnapshotFire;
- For the Firebase Admin SDK, you need to explicitly type the
DocumentReference
when usingwithConverter()
. This is due to the lack of automatic type inference when working with Firebase Admin.
const result = await db.collection(FirestoreCollections.Users)
.withConverter<FirestoreUser, FirestoreUserDb>(firestoreUserConverter)
.doc(user.uid)
.set({
email: user.email,
});
As you are using this method, you're telling TypeScript that the set()
operation will use the firestoreUserConverter and that the expected structure is FirestoreUser
when reading from Firestore, and FirestoreUserDb
when writing.
I hope this reference can help too: Property 'withConverter' does not exist on type 'AngularFirestoreDocument<unknown>, Firestore withConverter Returning Error for fromFirestore
If your focus is on maintaining strong type safety while working with Firestore, Typesaurus might help you avoid some of these issues with its built-in types, but if you're happy with your current setup and just want to get the converters to work seamlessly across both client and server, you can stick to Firestore's built-in typings.
If the issue persists, the other workaround is to use a function that ensures all collections are typed properly based on the converter provided. This function can be reused in both your frontend and backend code.
Function would look like this:
function getCollectionWithConverter<TFirestore, TDb>(
db: FirebaseFirestore.Firestore, // firebase-admin or @firebase/firestore
collectionName: string,
converter: FirestoreDataConverter<TFirestore, TDb>
): FirebaseFirestore.CollectionReference<TFirestore> {
return db.collection(collectionName).withConverter(converter);
}
TFirestore
would beFirestoreUser
(i.e., the format you want to work with in your app).TDb
would beFirestoreUserDb
(i.e., the format you use in Firestore, including any internal representations like timestamps).
When you call this function:
const userCollection = getCollectionWithConverter<FirestoreUser, FirestoreUserDb>(
db, FirestoreCollections.Users, firestoreUserConverter
);
You can also check this: Support for firestore CollectionReference#withConverter in AngularFirestoreCollection
I hope it helps you!