-
Notifications
You must be signed in to change notification settings - Fork 161
Closed
Labels
api: firestoreIssues related to the googleapis/nodejs-firestore API.Issues related to the googleapis/nodejs-firestore API.type: feature request‘Nice-to-have’ improvement, new feature or different behavior or design.‘Nice-to-have’ improvement, new feature or different behavior or design.
Description
Is your feature request related to a problem? Please describe.
This package works great with Typescript when using the .withConverter method and enforces correct types when performing a get() or set() or onSnapshot(), but fails to enforce types on the update() method.
With TS v4.1 we can now safely enforce that dot-notation strings as object keys are correct, so we can do better than:
export type UpdateData = {[fieldPath: string]: any};Check it out...
import { firestore } from "firebase-admin"
interface User {
name: string
email: string
address: {
line1: string
line2: string
postcode: string
verified: boolean
timeAtAddress: {
days: string
months: string
hours: string
}
}
}
// This helper function pipes my types through the converter
const converter = <T>() => ({
toFirestore: (data: Partial<T>) => data,
fromFirestore: (snap: FirebaseFirestore.QueryDocumentSnapshot) => snap.data() as T
})
const getUser = async () => {
const userDoc = await firestore().collection('user').withConverter(converter<User>()).doc('1234').get()
if (!userDoc.exists) return null
const userData = userDoc.data()
console.log(userData.email) // nice!
console.log(userData.unknownKey)
// TS Error: Property 'unknownKey' does not exist on type 'User'.
// Great!
}
const setUser = async () => {
await firestore().collection('user').withConverter(converter<User>()).doc('1234').set({
email: '',
name: ''
})
// TS Error: Property 'address' is missing in type '{ email: string; name: string; }' but required in type 'User'
// Great!
}
const setUserMerged = async () => {
await firestore().collection('user').withConverter(converter<User>()).doc('1234').set({
email: '',
name: '',
unknownKey: ''
}, { merge: true })
// TS Error: Object literal may only specify known properties, and 'unknownKey' does not exist in type 'Partial<User>'.
// Great!
}
const updateUserUnsafe = async () => {
await firestore().collection('user').withConverter(converter<User>()).doc('1234').update({
unknownKey: '', // no error here
'anything.I.like': true // no type error
})
}
// Here's how we could do an update with type safety:
type PathImpl<T, K extends keyof T> =
K extends string
? T[K] extends Record<string, any>
? T[K] extends ArrayLike<any>
? K | `${K}.${PathImpl<T[K], Exclude<keyof T[K], keyof any[]>>}`
: K | `${K}.${PathImpl<T[K], keyof 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<T[K]>
? PathValue<T[K], Rest>
: never
: never
: P extends keyof T
? T[P]
: never
type CustomUpdateData<T extends object> = Partial<{
[TKey in Path<T>]: PathValue<T, TKey>
}>
const updateUserSafe = async () => {
// Here I make the object first which provides some safety
const updatesObject: CustomUpdateData<User> = {
name: '',
'address.timeAtAddress.days': '',
'some.unknown.path': false,
// Object literal may only specify known properties, and ''some.unknown.path'' does not exist in type 'Partial<CustomUpdateData<User>>'
'address.verified': 'Oops! Wrong value type!'
// Type 'string' is not assignable to type 'boolean'
}
await firestore().collection('user').withConverter(converter<User>()).doc('1234').update(updatesObject)
}Describe the solution you'd like
I propose that the helper types for creating a type that is able to handle object paths should be added to FirebaseFirestore.UpdateData. I think that a solution could look like this:
// @google-cloud/firestore/types/firestore.d.ts
type PathImpl<T, K extends keyof T> =
K extends string
? T[K] extends Record<string, any>
? T[K] extends ArrayLike<any>
? K | `${K}.${PathImpl<T[K], Exclude<keyof T[K], keyof any[]>>}`
: K | `${K}.${PathImpl<T[K], keyof 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<T[K]>
? PathValue<T[K], Rest>
: never
: never
: P extends keyof T
? T[P]
: never
declare namespace FirebaseFirestore {
export type DocumentData = { [field: string]: any }
export type UpdateData<T = DocumentData> = Partial<{
[TKey in Path<T>]: PathValue<T, TKey>
}>
// ...
export class DocumentReference<T = DocumentData> {
// ...
update(data: UpdateData<T>, precondition?: Precondition): Promise<WriteResult>
}
}Which would allow type safety like this:
const updateSafe = async () => {
await firestore().collection('user').withConverter(converter<User>()).doc('1234').update({
name: '',
'address.line1': '',
'some.unknown.path': false,
// Object literal may only specify known properties, and ''some.unknown.path'' does not exist in type 'Partial<CustomUpdateData<User>>'
'address.verified': 'Oops! Wrong value type!'
// Type 'string' is not assignable to type 'boolean'
})
}cedricdg, christianrank, Gorthog, luzdealba, technologybrother and 11 morecedricdg, christianrank, technologybrother, nikglavin, pctfgm and 5 more
Metadata
Metadata
Assignees
Labels
api: firestoreIssues related to the googleapis/nodejs-firestore API.Issues related to the googleapis/nodejs-firestore API.type: feature request‘Nice-to-have’ improvement, new feature or different behavior or design.‘Nice-to-have’ improvement, new feature or different behavior or design.
