Skip to content

Instantly share code, notes, and snippets.

@JamieCurnow
Last active May 19, 2025 08:11
Show Gist options
  • Save JamieCurnow/cba3968a7f1e335d473632f9fc9f6e8b to your computer and use it in GitHub Desktop.
Save JamieCurnow/cba3968a7f1e335d473632f9fc9f6e8b to your computer and use it in GitHub Desktop.
Using Firestore with Typescript
/**
* 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 })
}
@drichar
Copy link

drichar commented Oct 3, 2023

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!')
}

@BenJackGill
Copy link

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-

@erayerdin
Copy link

Coming from google.

Seems like there are two FirestoreDataConverters, 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;
  }
}

@fabiomoggi
Copy link

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!

@mohsentaleb
Copy link

@erayerdin As of latest versions (firebase 11.0.1, firebase-admin 13.0.0) types for both FirestoreDataConverters are identical. Check out js & node.

@BenJackGill
Copy link

BenJackGill commented May 19, 2025

@erayerdin As of latest versions (firebase 11.0.1, firebase-admin 13.0.0) types for both FirestoreDataConverters 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.

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