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