From ef508b5236dbf301382b82d8ba5a4fe298dbaa8e Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Thu, 5 Nov 2026 09:23:24 -0800 Subject: [PATCH] docs(firestore): type reads with a converter instead of a type argument The README, the quickstart and the v7 upgrade guide showed collectionData(collection(firestore, 'items')), which fails with TS2345: collection() returns a reference typed DocumentData and the type argument asks for Item. docs/firestore.md cast the result with `as Observable` instead, which compiles but checks nothing. The examples now attach a FirestoreDataConverter through withConverter, the Firebase SDK's way to type a reference, with a fromFirestore that substitutes a default for a missing or wrong field. A new "Typed reads" section in docs/firestore.md says why the two shortcuts do not give a typed read, where the default-versus-throw choice lives, and that idField performs no validation. --- README.md | 17 ++++++++++++++--- docs/firestore.md | 40 ++++++++++++++++++++++++++++++++------- docs/install-and-setup.md | 17 ++++++++++++++--- docs/version-7-upgrade.md | 13 ++++++++----- 4 files changed, 69 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index b00574032..a47022fac 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,19 @@ export const appConfig: ApplicationConfig = { ```ts import { AsyncPipe } from '@angular/common'; import { Component, inject } from '@angular/core'; -import { Firestore, collectionData, collection } from '@angular/fire/firestore'; +import { Firestore, FirestoreDataConverter, collectionData, collection } from '@angular/fire/firestore'; interface Item { name: string; +} + +// Types the collection and checks every document on its way in +const itemConverter: FirestoreDataConverter = { + toFirestore: item => item, + fromFirestore: (snapshot, options) => { + const data = snapshot.data(options); + return { name: typeof data['name'] === 'string' ? data['name'] : '' }; + }, }; @Component({ @@ -56,11 +65,13 @@ interface Item { }) export class App { firestore = inject(Firestore); - itemCollection = collection(this.firestore, 'items'); - items$ = collectionData(this.itemCollection); + itemCollection = collection(this.firestore, 'items').withConverter(itemConverter); + items$ = collectionData(this.itemCollection); // Observable } ``` +The converter is what gives the read its type. `collection()` alone returns an untyped reference. See [Typed reads](docs/firestore.md#typed-reads). + ## Resources [Quickstart](docs/install-and-setup.md) - Get your first application up and running by following our quickstart guide. diff --git a/docs/firestore.md b/docs/firestore.md index 51a682329..551e56337 100644 --- a/docs/firestore.md +++ b/docs/firestore.md @@ -105,10 +105,27 @@ Let's explore reading data in Angular using the `collection` and `collectionData In `user-profile.component.ts`: ```typescript -import { Firestore, collection, collectionData} from '@angular/fire/firestore'; +import { Firestore, FirestoreDataConverter, collection, collectionData } from '@angular/fire/firestore'; import { Component, inject } from '@angular/core'; import { Observable } from 'rxjs'; +export interface UserProfile { + username: string; +} + +/* A converter translates between stored documents and your own type. + * fromFirestore runs on every document read, so it is where a missing + * or wrong field gets its default. */ +export const userProfileConverter: FirestoreDataConverter = { + toFirestore: profile => profile, + fromFirestore: (snapshot, options) => { + const data = snapshot.data(options); + return { + username: typeof data['username'] === 'string' ? data['username'] : '', + }; + }, +}; + @Component ({ selector: 'app-user-profile', standalone: true, @@ -119,17 +136,15 @@ export class UserProfileComponent { users$: Observable; constructor() { - // get a reference to the user-profile collection - const userProfileCollection = collection(this.firestore, 'users'); + // get a reference to the users collection, typed through the converter + const userProfileCollection = collection(this.firestore, 'users').withConverter(userProfileConverter); // get documents (data) from the collection using collectionData - this.users$ = collectionData(userProfileCollection) as Observable; + this.users$ = collectionData(userProfileCollection); // Observable } } -export interface UserProfile { - username: string; -} ``` + `collectionData` returns an `observable` that can we can use to display the data in the template. In `user-profile.component.html`: ```html @@ -145,6 +160,17 @@ export interface UserProfile { The `async` pipe handles unsubscribing from observables. +#### Typed reads + +`collection()` and `doc()` return references typed as `DocumentData`, and `collectionData()` and `docData()` return whatever type the reference carries (`docData()` adds `undefined` for a document that does not exist). Two tempting shortcuts do not give you a typed read: + +* `collectionData(collection(this.firestore, 'users'))` does not compile (`TS2345`), because the reference says `DocumentData` and the type argument says `UserProfile`. +* `collectionData(...) as Observable` compiles, but it only tells the compiler what you hope is stored. Nothing checks the documents. + +`withConverter()` is the Firebase SDK's way to type a reference. Attach a `FirestoreDataConverter` and every read through that reference returns `T`, with no type argument and no cast. Because `fromFirestore` runs on every document, it is also where you decide what happens to a document that does not match your type. The example above substitutes a default, so one malformed document does not stop the list from rendering. Throwing inside `fromFirestore` instead would error the `collectionData` observable at the first bad document, and the subscription would end. + +The `idField` option, `collectionData(ref, { idField: 'id' })`, copies each document's id onto the returned object. It does not validate any other field. With a converter you can do the same inside `fromFirestore` with `snapshot.id`. + ### Writing data To write to Cloud Firestore use the `addDoc` function. It will create a new document at the path specified by the collection. In `user-profile.component.ts`, we'll update the code to add a new document on a `