Skip to content

Instantly share code, notes, and snippets.

@JamieCurnow
Last active July 7, 2024 05:18
Show Gist options
  • Save JamieCurnow/650ea759c277757ae5665ea52400713b to your computer and use it in GitHub Desktop.
Save JamieCurnow/650ea759c277757ae5665ea52400713b to your computer and use it in GitHub Desktop.
Using Firestore with Typescript - including update helper
/**
* This Gist is part of a medium article - read here:
* https://jamiecurnow.medium.com/using-firestore-with-more-typescript-8058b6a88674
*/
// import firstore (obviously)
import { firestore } from 'firebase-admin'
import type { DocumentData, WithFieldValue } from 'firebase-admin/firestore'
// Here's the helper type for paths:
type PathImpl<T, K extends keyof T> = K extends string
? T[K] extends Record<string, any> | undefined
? T[K] extends ArrayLike<any>
? K | `${K}.${PathImpl<NonNullable<T[K]>, Exclude<keyof NonNullable<T[K]>, keyof any[]>>}`
: K | `${K}.${PathImpl<NonNullable<T[K]>, keyof NonNullable<T[K]>>}`
: K
: never
type Path<T> = PathImpl<T, keyof T> | keyof T
type PathValue<T, P extends Path<T>> = P extends `${infer K}.${infer Rest}`
? K extends keyof T
? Rest extends Path<NonNullable<T[K]>>
? PathValue<NonNullable<T[K]>, Rest>
: never
: never
: P extends keyof T
? NonNullable<T[P]>
: never
export type UpdateData<T extends object> = Partial<{
[TKey in Path<T>]: PathValue<T, TKey>
}>
// Import or define your types
// import { User } from '~/@types'
interface User {
name: string
email: string
address: {
line1: string
line2: string
postcode: string
verified: false
timeAtAddress: {
days: string
months: string
hours: string
}
}
}
interface Post {
something: boolean
somethingElse: boolean
}
// This helper function pipes your types through a firestore converter
const converter = <T>() => ({
toFirestore: (data: WithFieldValue<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 extends WithFieldValue<DocumentData>>(collectionPath: string) =>
firestore().collection(collectionPath).withConverter(converter<T>())
// Construct a database helper object
const db = {
// list your collections here
users: dataPoint<User>('users'),
userPosts: (userId: string) => dataPoint<Post>(`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 user = userDoc.data()
if (!user) return
const { address } = user
return address.line1
}
const createExample = async (userId: string) => {
await db.userPosts(userId).doc().create({
something: false,
somethingElse: true
})
}
const updateExample = async (id: string) => {
const updates: UpdateData<User> = {
'address.timeAtAddress.days': '',
name: ''
}
await db.users.doc(id).update(updates)
}
@espirin
Copy link

espirin commented Jun 14, 2024

I added dot notation for the update of optional fields. The original code would complain when trying to update a nested optional field using dot notation.

type PathImpl<T, K extends keyof T> = K extends string
  ? T[K] extends Record<string, any> | undefined
    ? T[K] extends ArrayLike<any>
      ? K | `${K}.${PathImpl<NonNullable<T[K]>, Exclude<keyof NonNullable<T[K]>, keyof any[]>>}`
      : K | `${K}.${PathImpl<NonNullable<T[K]>, keyof NonNullable<T[K]>>}`
    : K
  : never

type Path<T> = PathImpl<T, keyof T> | keyof T

type PathValue<T, P extends Path<T>> = P extends `${infer K}.${infer Rest}`
  ? K extends keyof T
    ? Rest extends Path<NonNullable<T[K]>>
      ? PathValue<NonNullable<T[K]>, Rest>
      : never
    : never
  : P extends keyof T
    ? NonNullable<T[P]>
    : never

export type UpdateData<T extends object> = Partial<{
  [TKey in Path<T>]: PathValue<T, TKey>
}>

@oscar-b
Copy link

oscar-b commented Jun 17, 2024

@JamieCurnow The gist fails when using "firebase-admin": "^12.1.0",, any idea on how to fix? Seems like the signature of withConverter() changed?

src/firestore.ts:68:55 - error TS2769: No overload matches this call.
  Overload 1 of 2, '(converter: null): CollectionReference<DocumentData, 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, DocumentData>): CollectionReference<T, 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 'FirestoreDataConverter<T, DocumentData>'.
      Types of property 'toFirestore' are incompatible.
        Type '(data: Partial<T>) => Partial<T>' is not assignable to type '{ (modelObject: WithFieldValue<T>): WithFieldValue<DocumentData>; (modelObject: PartialWithFieldValue<T>, options: SetOptions): PartialWithFieldValue<...>; }'.
          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] | undefined'.
                        Type 'FieldValue' is not assignable to type 'T[P]'.

68  firestore().collection(collectionPath).withConverter(converter<T>());
                                                         ~~~~~~~~~~~~~~

@JamieCurnow
Copy link
Author

Thanks @espirin - I've added that into the Gist 🙌

@oscar-b I've updated the gist now to work with firebase-admin ^12.1.1 which also adds support for FieldValue types

@oscar-b
Copy link

oscar-b commented Jun 17, 2024

@JamieCurnow Thanks, it works perfectly now!

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