Skip to content

Instantly share code, notes, and snippets.

@JamieCurnow
Last active April 14, 2024 01:42
Show Gist options
  • Star 56 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • 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 })
}
@spencercap
Copy link

🙌 NICE ! thanks @JamieCurnow

@yurist38
Copy link

This helped a lot! Thanks @JamieCurnow

@michaelmok2021
Copy link

@JamieCurnow can you add to this GIST an example to return all objects in the users collection? Thanks.

@smikheiev
Copy link

smikheiev commented Jan 11, 2022

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? 🤔

@tohagan
Copy link

tohagan commented Jan 20, 2022

@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()
}

@sandeepbol
Copy link

Why do you need to create a convert and call withConverter when you can simply cast the collection? All of the methods off of CollectionReference<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>`.
 */

@MattBevis
Copy link

MattBevis commented Oct 12, 2022

How do you bring the id with using the converter?

image

Like so?

@WingCH
Copy link

WingCH commented Nov 29, 2022

how to fix this error in typescript version 4.9.3?

image

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]'.

@TomKaltz
Copy link

how to fix this error in typescript version 4.9.3?

image
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?

@WingCH
Copy link

WingCH commented Feb 26, 2023

@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
});

@TomKaltz
Copy link

TomKaltz commented Mar 1, 2023

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

@tiagobnobrega
Copy link

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'.

@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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment