-
-
Save JamieCurnow/cba3968a7f1e335d473632f9fc9f6e8b to your computer and use it in GitHub Desktop.
/** | |
* This Gist is part of a medium article - read here: | |
* https://jamiecurnow.medium.com/using-firestore-with-typescript-65bd2a602945 | |
*/ | |
// import firstore (obviously) | |
import { firestore } from "firebase-admin" | |
// Import or define your types | |
// import { YourType } from '~/@types' | |
interface YourType { | |
firstName: string | |
lastName: string | |
isGreat: boolean | |
blackLivesMatter: true | |
} | |
interface YourOtherType { | |
something: boolean | |
somethingElse: boolean | |
} | |
// This helper function pipes your types through a firestore converter | |
const converter = <T>() => ({ | |
toFirestore: (data: Partial<T>) => data, | |
fromFirestore: (snap: FirebaseFirestore.QueryDocumentSnapshot) => snap.data() as T | |
}) | |
// This helper function exposes a 'typed' version of firestore().collection(collectionPath) | |
// Pass it a collectionPath string as the path to the collection in firestore | |
// Pass it a type argument representing the 'type' (schema) of the docs in the collection | |
const dataPoint = <T>(collectionPath: string) => firestore().collection(collectionPath).withConverter(converter<T>()) | |
// Construct a database helper object | |
const db = { | |
// list your collections here | |
users: dataPoint<YourType>('users'), | |
userPosts: (userId: string) => dataPoint<YourOtherType>(`users/${userId}/posts`) | |
} | |
// export your helper | |
export { db } | |
export default db | |
/** | |
* Some examples of how to use: | |
*/ | |
const example = async (id: string) => { | |
// firestore just as you know it, but with types | |
const userDoc = await db.users.doc(id).get() | |
const { blackLivesMatter } = userDoc.data() | |
return blackLivesMatter === true // obviously | |
} | |
const createExample = async (userId: string) => { | |
await db.userPosts(userId).doc().create({ | |
something: false, | |
somethingElse: true | |
}) | |
} | |
// Always use set for updates as firestore doesn't type update function correctly yet! | |
const updateExample = async (id: string) => { | |
await db.users.doc(id).set({ | |
firstName: 'Jamie', | |
blackLivesMatter: true | |
}, { merge: true }) | |
} |
🙌 NICE ! thanks @JamieCurnow
This helped a lot! Thanks @JamieCurnow
@JamieCurnow can you add to this GIST an example to return all objects in the users collection? Thanks.
Thanks, @JamieCurnow! Nice stuff!
What about serverTimestamp
? With serverTimestamp
, toFirestore
should have FieldValue
type, and fromFirestore
should have Timestamp
type, but converter
accepts only one data type. Is there a way to make it work? 🤔
@smikheiev For your timestamp fields use this typescript type ...
export type Timestamp = admin.firestore.Timestamp | admin.firestore.FieldValue;
... so you can assign a value like ...
{
...
updatedAt: FieldValue.serverTimestamp()
}
Why do you need to create a convert and call
withConverter
when you can simply cast the collection? All of the methods off ofCollectionReference<T>
are already typed (such as.doc
, etc.)const dataPoint = <T>(collectionPath: string) => firestore().collection(collectionPath) as CollectionReference<T>;
FireStore converters are intended to convert between the in-memory type representation and the persisted representation.
@MatthewLymer From documentation, it seems like the converters are precisely for this purpose.
/** * Called by the Firestore SDK to convert a custom model object of type T * into a plain Javascript object (suitable for writing directly to the * Firestore database). To use set() with `merge` and `mergeFields`, * toFirestore() must be defined with `Partial<T>`. */
how to fix this error in typescript version 4.9.3?
TS2769: No overload matches this call. Overload 1 of 2, '(converter: null): CollectionReference<DocumentData>', gave the following error. Argument of type '{ toFirestore: (data: Partial<T>) => Partial<T>; fromFirestore: (snap: FirebaseFirestore.QueryDocumentSnapshot) => T; }' is not assignable to parameter of type 'null'. Overload 2 of 2, '(converter: FirestoreDataConverter<T>): CollectionReference<T>', gave the following error. Argument of type '{ toFirestore: (data: Partial<T>) => Partial<T>; fromFirestore: (snap: FirebaseFirestore.QueryDocumentSnapshot) => T; }' is not assignable to parameter of type 'FirestoreDataConverter<T>'. Types of property 'toFirestore' are incompatible. Type '(data: Partial<T>) => Partial<T>' is not assignable to type '{ (modelObject: WithFieldValue<T>): DocumentData; (modelObject: PartialWithFieldValue<T>, options: SetOptions): DocumentData; }'. Types of parameters 'data' and 'modelObject' are incompatible. Type 'WithFieldValue<T>' is not assignable to type 'Partial<T>'. Type 'T extends Primitive ? T : T extends {} ? { [K in keyof T]: FieldValue | WithFieldValue<T[K]>; } : never' is not assignable to type 'Partial<T>'. Type 'T | (T extends {} ? { [K in keyof T]: FieldValue | WithFieldValue<T[K]>; } : never)' is not assignable to type 'Partial<T>'. Type 'T extends {} ? { [K in keyof T]: FieldValue | WithFieldValue<T[K]>; } : never' is not assignable to type 'Partial<T>'. Type '{ [K in keyof T]: FieldValue | WithFieldValue<T[K]>; }' is not assignable to type 'Partial<T>'. Type 'FieldValue | WithFieldValue<T[P]>' is not assignable to type 'T[P]'. Type 'FieldValue' is not assignable to type 'T[P]'.
how to fix this error in typescript version 4.9.3?
![]()
TS2769: No overload matches this call. Overload 1 of 2, '(converter: null): CollectionReference<DocumentData>', gave the following error. Argument of type '{ toFirestore: (data: Partial<T>) => Partial<T>; fromFirestore: (snap: FirebaseFirestore.QueryDocumentSnapshot) => T; }' is not assignable to parameter of type 'null'. Overload 2 of 2, '(converter: FirestoreDataConverter<T>): CollectionReference<T>', gave the following error. Argument of type '{ toFirestore: (data: Partial<T>) => Partial<T>; fromFirestore: (snap: FirebaseFirestore.QueryDocumentSnapshot) => T; }' is not assignable to parameter of type 'FirestoreDataConverter<T>'. Types of property 'toFirestore' are incompatible. Type '(data: Partial<T>) => Partial<T>' is not assignable to type '{ (modelObject: WithFieldValue<T>): DocumentData; (modelObject: PartialWithFieldValue<T>, options: SetOptions): DocumentData; }'. Types of parameters 'data' and 'modelObject' are incompatible. Type 'WithFieldValue<T>' is not assignable to type 'Partial<T>'. Type 'T extends Primitive ? T : T extends {} ? { [K in keyof T]: FieldValue | WithFieldValue<T[K]>; } : never' is not assignable to type 'Partial<T>'. Type 'T | (T extends {} ? { [K in keyof T]: FieldValue | WithFieldValue<T[K]>; } : never)' is not assignable to type 'Partial<T>'. Type 'T extends {} ? { [K in keyof T]: FieldValue | WithFieldValue<T[K]>; } : never' is not assignable to type 'Partial<T>'. Type '{ [K in keyof T]: FieldValue | WithFieldValue<T[K]>; }' is not assignable to type 'Partial<T>'. Type 'FieldValue | WithFieldValue<T[P]>' is not assignable to type 'T[P]'. Type 'FieldValue' is not assignable to type 'T[P]'.
I'm having the same issue. Has anyone figured this out?
@TomKaltz
I have modified the converter function, but I'm unsure if I did it correctly. Here's the new implementation
const converter = <T>(): FirestoreDataConverter<T> => ({
toFirestore: (data: T): FirebaseFirestore.DocumentData => {
return data as unknown as FirebaseFirestore.DocumentData;
},
fromFirestore: (snap: FirebaseFirestore.QueryDocumentSnapshot) => snap.data() as T
});
I found the following more recent article by @JamieCurnow to be very helpful!
https://plainenglish.io/blog/using-firestore-with-typescript-in-the-v9-sdk-cf36851bb099
My solution using the article mentioned by @TomKaltz:
import type { CollectionReference} from "firebase/firestore";
interface MyTypeA {
name:String;
}
interface MyTypeB {
name:String;
}
export interface AppCollectionsData{
"typeA":MyTypeA,
"typeB":MyTypeB
}
export type AppCollectionsNames = keyof AppCollectionsData;
const getCollection = <CName extends AppCollectionsNames>(collectionName:CName)=>collection(db,collectionName) as CollectionReference<AppCollectionsData[CName]>;
//===== Usage =====
const col = getCollection('typeA');
//type: CollectionReference<MyTypeA>
const snapshot = await getDocs(col);
//type: QuerySnapshot<MyTypeA>
const docs = snapshot.docs;
//type: QueryDocumentSnapshot<MyTypeA>[]
const col2 = getCollection('typeB');
//type: CollectionReference<MyTypeB>
const snapshot2 = await getDocs(col2);
//type: QuerySnapshot<MyTypeB>
const docs2 = snapshot2.docs;
//type: QueryDocumentSnapshot<MyTypeB>[]
const col3 = getCollection('typeAB');
//ERROR: TS2345: Argument of type '"typeAB"' is not assignable to parameter of type 'keyof AppCollectionsData'.
This uses Firebase v10.4.0, based on the withConverter
example in the docs: https://firebase.google.com/docs/firestore/query-data/get-data#custom_objects
Instead of getting the document, this just returns the reference. The document is converted to/from a model class instance.
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
getFirestore,
doc,
type FirestoreDataConverter,
type PartialWithFieldValue,
type DocumentData,
type QueryDocumentSnapshot
} from 'firebase/firestore'
import firebaseApp from '../config'
import { User } from './models/User'
const db = getFirestore(firebaseApp)
const converter = <T>(ModelClass: new (data: any) => T): FirestoreDataConverter<T> => ({
toFirestore: (data: PartialWithFieldValue<T>): PartialWithFieldValue<DocumentData> =>
data as PartialWithFieldValue<DocumentData>,
fromFirestore: (snapshot: QueryDocumentSnapshot<DocumentData>): T => {
const data = snapshot.data()
return new ModelClass(data) as T
}
})
const typedRef = <T>(ModelClass: new (data: any) => T, path: string, ...pathSegments: string[]) => {
return doc(db, path, ...pathSegments).withConverter(converter<T>(ModelClass))
}
const docRef = {
user: (uid: string) => typedRef<User>(User, 'users', uid)
}
export { docRef }
// Example
const ref = docRef.user(uid)
const docSnap = await getDoc(ref)
if (docSnap.exists()) {
// Convert to User
const user = docSnap.data()
// Use User instance method
console.log(user.toString())
} else {
console.log('No such document!')
}
Is anyone using the generic converter with VueFire? Would love to see how you adapted it to work with this: https://vuefire.vuejs.org/guide/realtime-data.html#Firestore-withConverter-
Coming from google.
Seems like there are two FirestoreDataConverter
s, one from @firebase/firestore
and the other from firebase-admin/firestore
.
And fromFirestore
implementation for each is totally different.
I'm using a shared codebase. My solution was this:
// rename the imports
import { FirestoreDataConverter as FrontendFirestoreDataConverter } from "@firebase/firestore";
import { FirestoreDataConverter as BackendFirestoreDataConverter, DocumentData, QueryDocumentSnapshot } from "firebase-admin/firestore";
// create different converters for frontend and backend
export const FrontendResourceConverter: FrontendFirestoreDataConverter<Resource> = {
fromFirestore(snapshot, options) {
return {
id: snapshot.id,
...snapshot.data(options) as Omit<Resource, "id">,
}
},
toFirestore(resource) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { id, ...rest } = resource;
return rest;
}
}
export const BackendResourceConverter: BackendFirestoreDataConverter<Resource> = {
fromFirestore(snapshot: QueryDocumentSnapshot<DocumentData>) { // notice how there's no options here
const data = snapshot.data();
return {
id: snapshot.id,
...data as Omit<Resource, "id">,
}
},
toFirestore(resource) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { id, ...rest } = resource;
return rest;
}
}
I can't destruct the typed object returning from doc.data().
export interface User {
id: number;
name: string;
email: string;
}
// omitting converter & dataPoint functions
const db = {
users: dataPoint<User>("users"),
};
Then in a Cloud Function I have:
const userDoc = await db.users.doc("12345").get();
const { email } = userDoc.data(); // error message: "Property 'email' does not exist on type 'Partial<User> | undefined'"
This should pretty much do the trick of having typed results from Firestore, however, the error message above keeps preventing typescript from compiling the code.
Any thoughts on this?
Thanks!
@erayerdin As of latest versions (firebase 11.0.1
, firebase-admin 13.0.0
) types for both FirestoreDataConverter
s are identical. Check out js & node.
@erayerdin As of latest versions (
firebase 11.0.1
,firebase-admin 13.0.0
) types for bothFirestoreDataConverter
s are identical. Check out js & node.
Note, if you're using modular Firebase this is not correct. The Types have subtle differences. But that comment might hold true if youre using the namespaced version.
- JavaScript Modular (v9): https://firebase.google.com/docs/reference/js/firestore_.firestoredataconverter
- JavaScript Namespaced (v8): https://firebase.google.com/docs/reference/js/v8/firebase.firestore.FirestoreDataConverter
- Node: https://firebase.google.com/docs/reference/node/firebase.firestore.FirestoreDataConverter
This is my current version which works well in my monorepo which has both front end (JS Modular) and backend (Node) setup. I am also using VueFire
so some extra stuff there for that but you can adjust as needed:
import type {
QueryDocumentSnapshot as BackendQueryDocumentSnapshot,
WithFieldValue as BackendWithFieldValue,
} from "firebase-admin/firestore";
import type {
DocumentData,
QueryDocumentSnapshot as FrontendQueryDocumentSnapshot,
WithFieldValue as FrontendWithFieldValue,
SnapshotOptions,
} from "firebase/firestore";
// This custom converter function adds a non-enumerable 'id' property to Firestore documents, a VueFire requirement.
// Unlike VueFire's default converter that handles 'null' values, this implementation assumes Firestore collections automatically exclude non-existent documents, eliminating the need for 'null'.
// This results in cleaner types for VueFire's useCollection() like "Ref<WithId<User>[]>", and aligns with useDocument()'s return type "_RefFirestore<WithId<User> | undefined>", which already accounts for potentially missing documents using undefined.
// The generic type <T extends DocumentData> also ensures flexibility and type safety across various Firestore collections.
// Reference to VueFire's default converter: https://github.com/vuejs/vuefire/blob/1e2c71e88c28e886e701f4e41ad25973a1945c2a/src/firestore/utils.ts#L20
export const frontendConverter = <T extends DocumentData>() => ({
// Add non-enumerable 'id' property to Firestore documents, which is a requirement for VueFire
fromFirestore: (
snapshot: FrontendQueryDocumentSnapshot<T, DocumentData>, // Firebase V9 requires two type arguments
options?: SnapshotOptions,
) => {
return Object.defineProperties(snapshot.data(options), {
id: { value: snapshot.id },
});
},
// This is okay because the "id" added below is non-enumerable and therefore will not be sent to Firestore
toFirestore: (data: FrontendWithFieldValue<T>) => data,
});
// This is a similar and simpler version of the frontendConverter above
// Backend version is required because of different Firestore SDKs and types
export const backendConverter = <T>() => ({
fromFirestore: (snapshot: BackendQueryDocumentSnapshot<T>) => snapshot.data(),
toFirestore: (data: BackendWithFieldValue<T>) => data,
});
This uses Firebase v10.4.0, based on the
withConverter
example in the docs: https://firebase.google.com/docs/firestore/query-data/get-data#custom_objectsInstead of getting the document, this just returns the reference. The document is converted to/from a model class instance.
/* eslint-disable @typescript-eslint/no-explicit-any */ import { getFirestore, doc, type FirestoreDataConverter, type PartialWithFieldValue, type DocumentData, type QueryDocumentSnapshot } from 'firebase/firestore' import firebaseApp from '../config' import { User } from './models/User' const db = getFirestore(firebaseApp) const converter = <T>(ModelClass: new (data: any) => T): FirestoreDataConverter<T> => ({ toFirestore: (data: PartialWithFieldValue<T>): PartialWithFieldValue<DocumentData> => data as PartialWithFieldValue<DocumentData>, fromFirestore: (snapshot: QueryDocumentSnapshot<DocumentData>): T => { const data = snapshot.data() return new ModelClass(data) as T } }) const typedRef = <T>(ModelClass: new (data: any) => T, path: string, ...pathSegments: string[]) => { return doc(db, path, ...pathSegments).withConverter(converter<T>(ModelClass)) } const docRef = { user: (uid: string) => typedRef<User>(User, 'users', uid) } export { docRef } // Example const ref = docRef.user(uid) const docSnap = await getDoc(ref) if (docSnap.exists()) { // Convert to User const user = docSnap.data() // Use User instance method console.log(user.toString()) } else { console.log('No such document!') }
@drichar hi, this is very nice function. Actually I have a question about this, if the data class model have a Date type, when get the data from forestore, it should transfer Timestamp type to typescript Date type. Another question is that, how to support FieldValue type for data class model. For example:
class User {
name: string;
age: number;
createAt: Date
}
const user = new User("daniel", 23, new Date())
// acutually I want to support **new User("daniel", FielValue.increment(1), FielValue.serverTimestamp())**,
it means any field in the class maybe can be FieldValue type
docRef.set(user)
Why do you need to create a convert and call
withConverter
when you can simply assert the type of the collection? All of the methods off ofCollectionReference<T>
are already typed (such as.doc
, etc.)FireStore converters are intended to convert between the in-memory type representation and the persisted representation, not simply asserting the type.