Last active
July 7, 2024 05:18
-
-
Save JamieCurnow/650ea759c277757ae5665ea52400713b to your computer and use it in GitHub Desktop.
Using Firestore with Typescript - including update helper
This file contains 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-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 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 Thanks, it works perfectly now!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.