Last active
May 19, 2025 08:11
-
-
Save JamieCurnow/cba3968a7f1e335d473632f9fc9f6e8b to your computer and use it in GitHub Desktop.
Using Firestore with Typescript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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 }) | |
} |
@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,
});
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@erayerdin As of latest versions (
firebase 11.0.1
,firebase-admin 13.0.0
) types for bothFirestoreDataConverter
s are identical. Check out js & node.