diff --git a/.changeset/quick-peas-guess.md b/.changeset/quick-peas-guess.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/quick-peas-guess.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/docs-devsite/firestore_.firestoredataconverter.md b/docs-devsite/firestore_.firestoredataconverter.md index 700d0f38aaa..166cc09214e 100644 --- a/docs-devsite/firestore_.firestoredataconverter.md +++ b/docs-devsite/firestore_.firestoredataconverter.md @@ -98,43 +98,162 @@ toFirestore(modelObject: PartialWithFieldValue, options: SetOption ### Example +Simple Example ```typescript -class Post { - constructor(readonly title: string, readonly author: string) {} +const numberConverter = { + toFirestore(value: WithFieldValue) { + return { value }; + }, + fromFirestore(snapshot: QueryDocumentSnapshot, options: SnapshotOptions) { + return snapshot.data(options).value as number; + } +}; + +async function simpleDemo(db: Firestore): Promise { + const documentRef = doc(db, 'values/value123').withConverter(numberConverter); + + // converters are used with `setDoc`, `addDoc`, and `getDoc` + await setDoc(documentRef, 42); + const snapshot1 = await getDoc(documentRef); + assertEqual(snapshot1.data(), 42); + + // converters are not used when writing data with `updateDoc` + await updateDoc(documentRef, { value: 999 }); + const snapshot2 = await getDoc(documentRef); + assertEqual(snapshot2.data(), 999); +} - toString(): string { - return this.title + ', by ' + this.author; - } +``` +Advanced Example + +```typescript +// The Post class is a model that is used by our application. +// This class may have properties and methods that are specific +// to our application execution, which do not need to be persisted +// to Firestore. +class Post { + constructor( + readonly title: string, + readonly author: string, + readonly lastUpdatedMillis: number + ) {} + toString(): string { + return `${this.title} by ${this.author}`; + } } +// The PostDbModel represents how we want our posts to be stored +// in Firestore. This DbModel has different properties (`ttl`, +// `aut`, and `lut`) from the Post class we use in our application. interface PostDbModel { - title: string; - author: string; + ttl: string; + aut: { firstName: string; lastName: string }; + lut: Timestamp; } -const postConverter = { - toFirestore(post: WithFieldValue): PostDbModel { - return {title: post.title, author: post.author}; - }, - fromFirestore( - snapshot: QueryDocumentSnapshot, - options: SnapshotOptions - ): Post { - const data = snapshot.data(options) as PostDbModel; - return new Post(data.title, data.author); - } -}; +// The `PostConverter` implements `FirestoreDataConverter` and specifies +// how the Firestore SDK can convert `Post` objects to `PostDbModel` +// objects and vice versa. +class PostConverter implements FirestoreDataConverter { + toFirestore(post: WithFieldValue): WithFieldValue { + return { + ttl: post.title, + aut: this._autFromAuthor(post.author), + lut: this._lutFromLastUpdatedMillis(post.lastUpdatedMillis) + }; + } + + fromFirestore(snapshot: QueryDocumentSnapshot, options: SnapshotOptions): Post { + const data = snapshot.data(options) as PostDbModel; + const author = `${data.aut.firstName} ${data.aut.lastName}`; + return new Post(data.ttl, author, data.lut.toMillis()); + } + + _autFromAuthor( + author: string | FieldValue + ): { firstName: string; lastName: string } | FieldValue { + if (typeof author !== 'string') { + // `author` is a FieldValue, so just return it. + return author; + } + const [firstName, lastName] = author.split(' '); + return {firstName, lastName}; + } + + _lutFromLastUpdatedMillis( + lastUpdatedMillis: number | FieldValue + ): Timestamp | FieldValue { + if (typeof lastUpdatedMillis !== 'number') { + // `lastUpdatedMillis` must be a FieldValue, so just return it. + return lastUpdatedMillis; + } + return Timestamp.fromMillis(lastUpdatedMillis); + } +} -const postSnap = await firebase.firestore() - .collection('posts') - .withConverter(postConverter) - .doc().get(); -const post = postSnap.data(); -if (post !== undefined) { - post.title; // string - post.toString(); // Should be defined - post.someNonExistentProperty; // TS error +async function advancedDemo(db: Firestore): Promise { + // Create a `DocumentReference` with a `FirestoreDataConverter`. + const documentRef = doc(db, 'posts/post123').withConverter(new PostConverter()); + + // The `data` argument specified to `setDoc()` is type checked by the + // TypeScript compiler to be compatible with `Post`. Since the `data` + // argument is typed as `WithFieldValue` rather than just `Post`, + // this allows properties of the `data` argument to also be special + // Firestore values that perform server-side mutations, such as + // `arrayRemove()`, `deleteField()`, and `serverTimestamp()`. + await setDoc(documentRef, { + title: 'My Life', + author: 'Foo Bar', + lastUpdatedMillis: serverTimestamp() + }); + + // The TypeScript compiler will fail to compile if the `data` argument to + // `setDoc()` is _not_ compatible with `WithFieldValue`. This + // type checking prevents the caller from specifying objects with incorrect + // properties or property values. + // @ts-expect-error "Argument of type { ttl: string; } is not assignable + // to parameter of type WithFieldValue" + await setDoc(documentRef, { ttl: 'The Title' }); + + // When retrieving a document with `getDoc()` the `DocumentSnapshot` + // object's `data()` method returns a `Post`, rather than a generic object, + // which would have been returned if the `DocumentReference` did _not_ have a + // `FirestoreDataConverter` attached to it. + const snapshot1: DocumentSnapshot = await getDoc(documentRef); + const post1: Post = snapshot1.data()!; + if (post1) { + assertEqual(post1.title, 'My Life'); + assertEqual(post1.author, 'Foo Bar'); + } + + // The `data` argument specified to `updateDoc()` is type checked by the + // TypeScript compiler to be compatible with `PostDbModel`. Note that + // unlike `setDoc()`, whose `data` argument must be compatible with `Post`, + // the `data` argument to `updateDoc()` must be compatible with + // `PostDbModel`. Similar to `setDoc()`, since the `data` argument is typed + // as `WithFieldValue` rather than just `PostDbModel`, this + // allows properties of the `data` argument to also be those special + // Firestore values, like `arrayRemove()`, `deleteField()`, and + // `serverTimestamp()`. + await updateDoc(documentRef, { + 'aut.firstName': 'NewFirstName', + lut: serverTimestamp() + }); + + // The TypeScript compiler will fail to compile if the `data` argument to + // `updateDoc()` is _not_ compatible with `WithFieldValue`. + // This type checking prevents the caller from specifying objects with + // incorrect properties or property values. + // @ts-expect-error "Argument of type { title: string; } is not assignable + // to parameter of type WithFieldValue" + await updateDoc(documentRef, { title: 'New Title' }); + const snapshot2: DocumentSnapshot = await getDoc(documentRef); + const post2: Post = snapshot2.data()!; + if (post2) { + assertEqual(post2.title, 'My Life'); + assertEqual(post2.author, 'NewFirstName Bar'); + } } ``` diff --git a/docs-devsite/firestore_lite.firestoredataconverter.md b/docs-devsite/firestore_lite.firestoredataconverter.md index 660f04f371f..22a22c71253 100644 --- a/docs-devsite/firestore_lite.firestoredataconverter.md +++ b/docs-devsite/firestore_lite.firestoredataconverter.md @@ -97,40 +97,162 @@ toFirestore(modelObject: PartialWithFieldValue, options: SetOption ### Example +Simple Example ```typescript -class Post { - constructor(readonly title: string, readonly author: string) {} +const numberConverter = { + toFirestore(value: WithFieldValue) { + return { value }; + }, + fromFirestore(snapshot: QueryDocumentSnapshot, options: SnapshotOptions) { + return snapshot.data(options).value as number; + } +}; + +async function simpleDemo(db: Firestore): Promise { + const documentRef = doc(db, 'values/value123').withConverter(numberConverter); + + // converters are used with `setDoc`, `addDoc`, and `getDoc` + await setDoc(documentRef, 42); + const snapshot1 = await getDoc(documentRef); + assertEqual(snapshot1.data(), 42); + + // converters are not used when writing data with `updateDoc` + await updateDoc(documentRef, { value: 999 }); + const snapshot2 = await getDoc(documentRef); + assertEqual(snapshot2.data(), 999); +} - toString(): string { - return this.title + ', by ' + this.author; - } +``` +Advanced Example + +```typescript +// The Post class is a model that is used by our application. +// This class may have properties and methods that are specific +// to our application execution, which do not need to be persisted +// to Firestore. +class Post { + constructor( + readonly title: string, + readonly author: string, + readonly lastUpdatedMillis: number + ) {} + toString(): string { + return `${this.title} by ${this.author}`; + } } +// The PostDbModel represents how we want our posts to be stored +// in Firestore. This DbModel has different properties (`ttl`, +// `aut`, and `lut`) from the Post class we use in our application. interface PostDbModel { - title: string; - author: string; + ttl: string; + aut: { firstName: string; lastName: string }; + lut: Timestamp; } -const postConverter = { - toFirestore(post: WithFieldValue): PostDbModel { - return {title: post.title, author: post.author}; - }, - fromFirestore(snapshot: QueryDocumentSnapshot): Post { - const data = snapshot.data(options) as PostDbModel; - return new Post(data.title, data.author); - } -}; +// The `PostConverter` implements `FirestoreDataConverter` and specifies +// how the Firestore SDK can convert `Post` objects to `PostDbModel` +// objects and vice versa. +class PostConverter implements FirestoreDataConverter { + toFirestore(post: WithFieldValue): WithFieldValue { + return { + ttl: post.title, + aut: this._autFromAuthor(post.author), + lut: this._lutFromLastUpdatedMillis(post.lastUpdatedMillis) + }; + } + + fromFirestore(snapshot: QueryDocumentSnapshot, options: SnapshotOptions): Post { + const data = snapshot.data(options) as PostDbModel; + const author = `${data.aut.firstName} ${data.aut.lastName}`; + return new Post(data.ttl, author, data.lut.toMillis()); + } + + _autFromAuthor( + author: string | FieldValue + ): { firstName: string; lastName: string } | FieldValue { + if (typeof author !== 'string') { + // `author` is a FieldValue, so just return it. + return author; + } + const [firstName, lastName] = author.split(' '); + return {firstName, lastName}; + } + + _lutFromLastUpdatedMillis( + lastUpdatedMillis: number | FieldValue + ): Timestamp | FieldValue { + if (typeof lastUpdatedMillis !== 'number') { + // `lastUpdatedMillis` must be a FieldValue, so just return it. + return lastUpdatedMillis; + } + return Timestamp.fromMillis(lastUpdatedMillis); + } +} -const postSnap = await firebase.firestore() - .collection('posts') - .withConverter(postConverter) - .doc().get(); -const post = postSnap.data(); -if (post !== undefined) { - post.title; // string - post.toString(); // Should be defined - post.someNonExistentProperty; // TS error +async function advancedDemo(db: Firestore): Promise { + // Create a `DocumentReference` with a `FirestoreDataConverter`. + const documentRef = doc(db, 'posts/post123').withConverter(new PostConverter()); + + // The `data` argument specified to `setDoc()` is type checked by the + // TypeScript compiler to be compatible with `Post`. Since the `data` + // argument is typed as `WithFieldValue` rather than just `Post`, + // this allows properties of the `data` argument to also be special + // Firestore values that perform server-side mutations, such as + // `arrayRemove()`, `deleteField()`, and `serverTimestamp()`. + await setDoc(documentRef, { + title: 'My Life', + author: 'Foo Bar', + lastUpdatedMillis: serverTimestamp() + }); + + // The TypeScript compiler will fail to compile if the `data` argument to + // `setDoc()` is _not_ compatible with `WithFieldValue`. This + // type checking prevents the caller from specifying objects with incorrect + // properties or property values. + // @ts-expect-error "Argument of type { ttl: string; } is not assignable + // to parameter of type WithFieldValue" + await setDoc(documentRef, { ttl: 'The Title' }); + + // When retrieving a document with `getDoc()` the `DocumentSnapshot` + // object's `data()` method returns a `Post`, rather than a generic object, + // which would have been returned if the `DocumentReference` did _not_ have a + // `FirestoreDataConverter` attached to it. + const snapshot1: DocumentSnapshot = await getDoc(documentRef); + const post1: Post = snapshot1.data()!; + if (post1) { + assertEqual(post1.title, 'My Life'); + assertEqual(post1.author, 'Foo Bar'); + } + + // The `data` argument specified to `updateDoc()` is type checked by the + // TypeScript compiler to be compatible with `PostDbModel`. Note that + // unlike `setDoc()`, whose `data` argument must be compatible with `Post`, + // the `data` argument to `updateDoc()` must be compatible with + // `PostDbModel`. Similar to `setDoc()`, since the `data` argument is typed + // as `WithFieldValue` rather than just `PostDbModel`, this + // allows properties of the `data` argument to also be those special + // Firestore values, like `arrayRemove()`, `deleteField()`, and + // `serverTimestamp()`. + await updateDoc(documentRef, { + 'aut.firstName': 'NewFirstName', + lut: serverTimestamp() + }); + + // The TypeScript compiler will fail to compile if the `data` argument to + // `updateDoc()` is _not_ compatible with `WithFieldValue`. + // This type checking prevents the caller from specifying objects with + // incorrect properties or property values. + // @ts-expect-error "Argument of type { title: string; } is not assignable + // to parameter of type WithFieldValue" + await updateDoc(documentRef, { title: 'New Title' }); + const snapshot2: DocumentSnapshot = await getDoc(documentRef); + const post2: Post = snapshot2.data()!; + if (post2) { + assertEqual(post2.title, 'My Life'); + assertEqual(post2.author, 'NewFirstName Bar'); + } } ``` diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index 9c266d91867..fe74e03bcf0 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -49,42 +49,163 @@ import { SnapshotListenOptions } from './reference_impl'; * storing and retrieving objects from Firestore. * * @example + * + * Simple Example + * * ```typescript - * class Post { - * constructor(readonly title: string, readonly author: string) {} + * const numberConverter = { + * toFirestore(value: WithFieldValue) { + * return { value }; + * }, + * fromFirestore(snapshot: QueryDocumentSnapshot, options: SnapshotOptions) { + * return snapshot.data(options).value as number; + * } + * }; + * + * async function simpleDemo(db: Firestore): Promise { + * const documentRef = doc(db, 'values/value123').withConverter(numberConverter); + * + * // converters are used with `setDoc`, `addDoc`, and `getDoc` + * await setDoc(documentRef, 42); + * const snapshot1 = await getDoc(documentRef); + * assertEqual(snapshot1.data(), 42); * - * toString(): string { - * return this.title + ', by ' + this.author; - * } + * // converters are not used when writing data with `updateDoc` + * await updateDoc(documentRef, { value: 999 }); + * const snapshot2 = await getDoc(documentRef); + * assertEqual(snapshot2.data(), 999); + * } + * ``` + * + * Advanced Example + * + * ```typescript + * // The Post class is a model that is used by our application. + * // This class may have properties and methods that are specific + * // to our application execution, which do not need to be persisted + * // to Firestore. + * class Post { + * constructor( + * readonly title: string, + * readonly author: string, + * readonly lastUpdatedMillis: number + * ) {} + * toString(): string { + * return `${this.title} by ${this.author}`; + * } * } * + * // The PostDbModel represents how we want our posts to be stored + * // in Firestore. This DbModel has different properties (`ttl`, + * // `aut`, and `lut`) from the Post class we use in our application. * interface PostDbModel { - * title: string; - * author: string; + * ttl: string; + * aut: { firstName: string; lastName: string }; + * lut: Timestamp; * } * - * const postConverter = { - * toFirestore(post: WithFieldValue): PostDbModel { - * return {title: post.title, author: post.author}; - * }, - * fromFirestore( - * snapshot: QueryDocumentSnapshot, - * options: SnapshotOptions - * ): Post { - * const data = snapshot.data(options) as PostDbModel; - * return new Post(data.title, data.author); - * } - * }; + * // The `PostConverter` implements `FirestoreDataConverter` and specifies + * // how the Firestore SDK can convert `Post` objects to `PostDbModel` + * // objects and vice versa. + * class PostConverter implements FirestoreDataConverter { + * toFirestore(post: WithFieldValue): WithFieldValue { + * return { + * ttl: post.title, + * aut: this._autFromAuthor(post.author), + * lut: this._lutFromLastUpdatedMillis(post.lastUpdatedMillis) + * }; + * } + * + * fromFirestore(snapshot: QueryDocumentSnapshot, options: SnapshotOptions): Post { + * const data = snapshot.data(options) as PostDbModel; + * const author = `${data.aut.firstName} ${data.aut.lastName}`; + * return new Post(data.ttl, author, data.lut.toMillis()); + * } + * + * _autFromAuthor( + * author: string | FieldValue + * ): { firstName: string; lastName: string } | FieldValue { + * if (typeof author !== 'string') { + * // `author` is a FieldValue, so just return it. + * return author; + * } + * const [firstName, lastName] = author.split(' '); + * return {firstName, lastName}; + * } + * + * _lutFromLastUpdatedMillis( + * lastUpdatedMillis: number | FieldValue + * ): Timestamp | FieldValue { + * if (typeof lastUpdatedMillis !== 'number') { + * // `lastUpdatedMillis` must be a FieldValue, so just return it. + * return lastUpdatedMillis; + * } + * return Timestamp.fromMillis(lastUpdatedMillis); + * } + * } + * + * async function advancedDemo(db: Firestore): Promise { + * // Create a `DocumentReference` with a `FirestoreDataConverter`. + * const documentRef = doc(db, 'posts/post123').withConverter(new PostConverter()); + * + * // The `data` argument specified to `setDoc()` is type checked by the + * // TypeScript compiler to be compatible with `Post`. Since the `data` + * // argument is typed as `WithFieldValue` rather than just `Post`, + * // this allows properties of the `data` argument to also be special + * // Firestore values that perform server-side mutations, such as + * // `arrayRemove()`, `deleteField()`, and `serverTimestamp()`. + * await setDoc(documentRef, { + * title: 'My Life', + * author: 'Foo Bar', + * lastUpdatedMillis: serverTimestamp() + * }); + * + * // The TypeScript compiler will fail to compile if the `data` argument to + * // `setDoc()` is _not_ compatible with `WithFieldValue`. This + * // type checking prevents the caller from specifying objects with incorrect + * // properties or property values. + * // @ts-expect-error "Argument of type { ttl: string; } is not assignable + * // to parameter of type WithFieldValue" + * await setDoc(documentRef, { ttl: 'The Title' }); + * + * // When retrieving a document with `getDoc()` the `DocumentSnapshot` + * // object's `data()` method returns a `Post`, rather than a generic object, + * // which would have been returned if the `DocumentReference` did _not_ have a + * // `FirestoreDataConverter` attached to it. + * const snapshot1: DocumentSnapshot = await getDoc(documentRef); + * const post1: Post = snapshot1.data()!; + * if (post1) { + * assertEqual(post1.title, 'My Life'); + * assertEqual(post1.author, 'Foo Bar'); + * } + * + * // The `data` argument specified to `updateDoc()` is type checked by the + * // TypeScript compiler to be compatible with `PostDbModel`. Note that + * // unlike `setDoc()`, whose `data` argument must be compatible with `Post`, + * // the `data` argument to `updateDoc()` must be compatible with + * // `PostDbModel`. Similar to `setDoc()`, since the `data` argument is typed + * // as `WithFieldValue` rather than just `PostDbModel`, this + * // allows properties of the `data` argument to also be those special + * // Firestore values, like `arrayRemove()`, `deleteField()`, and + * // `serverTimestamp()`. + * await updateDoc(documentRef, { + * 'aut.firstName': 'NewFirstName', + * lut: serverTimestamp() + * }); * - * const postSnap = await firebase.firestore() - * .collection('posts') - * .withConverter(postConverter) - * .doc().get(); - * const post = postSnap.data(); - * if (post !== undefined) { - * post.title; // string - * post.toString(); // Should be defined - * post.someNonExistentProperty; // TS error + * // The TypeScript compiler will fail to compile if the `data` argument to + * // `updateDoc()` is _not_ compatible with `WithFieldValue`. + * // This type checking prevents the caller from specifying objects with + * // incorrect properties or property values. + * // @ts-expect-error "Argument of type { title: string; } is not assignable + * // to parameter of type WithFieldValue" + * await updateDoc(documentRef, { title: 'New Title' }); + * const snapshot2: DocumentSnapshot = await getDoc(documentRef); + * const post2: Post = snapshot2.data()!; + * if (post2) { + * assertEqual(post2.title, 'My Life'); + * assertEqual(post2.author, 'NewFirstName Bar'); + * } * } * ``` */ diff --git a/packages/firestore/src/lite-api/snapshot.ts b/packages/firestore/src/lite-api/snapshot.ts index 5d243cabc25..e1fe19522db 100644 --- a/packages/firestore/src/lite-api/snapshot.ts +++ b/packages/firestore/src/lite-api/snapshot.ts @@ -47,39 +47,163 @@ import { AbstractUserDataWriter } from './user_data_writer'; * storing and retrieving objects from Firestore. * * @example + * + * Simple Example + * * ```typescript - * class Post { - * constructor(readonly title: string, readonly author: string) {} + * const numberConverter = { + * toFirestore(value: WithFieldValue) { + * return { value }; + * }, + * fromFirestore(snapshot: QueryDocumentSnapshot, options: SnapshotOptions) { + * return snapshot.data(options).value as number; + * } + * }; + * + * async function simpleDemo(db: Firestore): Promise { + * const documentRef = doc(db, 'values/value123').withConverter(numberConverter); + * + * // converters are used with `setDoc`, `addDoc`, and `getDoc` + * await setDoc(documentRef, 42); + * const snapshot1 = await getDoc(documentRef); + * assertEqual(snapshot1.data(), 42); * - * toString(): string { - * return this.title + ', by ' + this.author; - * } + * // converters are not used when writing data with `updateDoc` + * await updateDoc(documentRef, { value: 999 }); + * const snapshot2 = await getDoc(documentRef); + * assertEqual(snapshot2.data(), 999); + * } + * ``` + * + * Advanced Example + * + * ```typescript + * // The Post class is a model that is used by our application. + * // This class may have properties and methods that are specific + * // to our application execution, which do not need to be persisted + * // to Firestore. + * class Post { + * constructor( + * readonly title: string, + * readonly author: string, + * readonly lastUpdatedMillis: number + * ) {} + * toString(): string { + * return `${this.title} by ${this.author}`; + * } * } * + * // The PostDbModel represents how we want our posts to be stored + * // in Firestore. This DbModel has different properties (`ttl`, + * // `aut`, and `lut`) from the Post class we use in our application. * interface PostDbModel { - * title: string; - * author: string; + * ttl: string; + * aut: { firstName: string; lastName: string }; + * lut: Timestamp; * } * - * const postConverter = { - * toFirestore(post: WithFieldValue): PostDbModel { - * return {title: post.title, author: post.author}; - * }, - * fromFirestore(snapshot: QueryDocumentSnapshot): Post { - * const data = snapshot.data(options) as PostDbModel; - * return new Post(data.title, data.author); - * } - * }; + * // The `PostConverter` implements `FirestoreDataConverter` and specifies + * // how the Firestore SDK can convert `Post` objects to `PostDbModel` + * // objects and vice versa. + * class PostConverter implements FirestoreDataConverter { + * toFirestore(post: WithFieldValue): WithFieldValue { + * return { + * ttl: post.title, + * aut: this._autFromAuthor(post.author), + * lut: this._lutFromLastUpdatedMillis(post.lastUpdatedMillis) + * }; + * } + * + * fromFirestore(snapshot: QueryDocumentSnapshot, options: SnapshotOptions): Post { + * const data = snapshot.data(options) as PostDbModel; + * const author = `${data.aut.firstName} ${data.aut.lastName}`; + * return new Post(data.ttl, author, data.lut.toMillis()); + * } + * + * _autFromAuthor( + * author: string | FieldValue + * ): { firstName: string; lastName: string } | FieldValue { + * if (typeof author !== 'string') { + * // `author` is a FieldValue, so just return it. + * return author; + * } + * const [firstName, lastName] = author.split(' '); + * return {firstName, lastName}; + * } + * + * _lutFromLastUpdatedMillis( + * lastUpdatedMillis: number | FieldValue + * ): Timestamp | FieldValue { + * if (typeof lastUpdatedMillis !== 'number') { + * // `lastUpdatedMillis` must be a FieldValue, so just return it. + * return lastUpdatedMillis; + * } + * return Timestamp.fromMillis(lastUpdatedMillis); + * } + * } + * + * async function advancedDemo(db: Firestore): Promise { + * // Create a `DocumentReference` with a `FirestoreDataConverter`. + * const documentRef = doc(db, 'posts/post123').withConverter(new PostConverter()); + * + * // The `data` argument specified to `setDoc()` is type checked by the + * // TypeScript compiler to be compatible with `Post`. Since the `data` + * // argument is typed as `WithFieldValue` rather than just `Post`, + * // this allows properties of the `data` argument to also be special + * // Firestore values that perform server-side mutations, such as + * // `arrayRemove()`, `deleteField()`, and `serverTimestamp()`. + * await setDoc(documentRef, { + * title: 'My Life', + * author: 'Foo Bar', + * lastUpdatedMillis: serverTimestamp() + * }); + * + * // The TypeScript compiler will fail to compile if the `data` argument to + * // `setDoc()` is _not_ compatible with `WithFieldValue`. This + * // type checking prevents the caller from specifying objects with incorrect + * // properties or property values. + * // @ts-expect-error "Argument of type { ttl: string; } is not assignable + * // to parameter of type WithFieldValue" + * await setDoc(documentRef, { ttl: 'The Title' }); + * + * // When retrieving a document with `getDoc()` the `DocumentSnapshot` + * // object's `data()` method returns a `Post`, rather than a generic object, + * // which would have been returned if the `DocumentReference` did _not_ have a + * // `FirestoreDataConverter` attached to it. + * const snapshot1: DocumentSnapshot = await getDoc(documentRef); + * const post1: Post = snapshot1.data()!; + * if (post1) { + * assertEqual(post1.title, 'My Life'); + * assertEqual(post1.author, 'Foo Bar'); + * } + * + * // The `data` argument specified to `updateDoc()` is type checked by the + * // TypeScript compiler to be compatible with `PostDbModel`. Note that + * // unlike `setDoc()`, whose `data` argument must be compatible with `Post`, + * // the `data` argument to `updateDoc()` must be compatible with + * // `PostDbModel`. Similar to `setDoc()`, since the `data` argument is typed + * // as `WithFieldValue` rather than just `PostDbModel`, this + * // allows properties of the `data` argument to also be those special + * // Firestore values, like `arrayRemove()`, `deleteField()`, and + * // `serverTimestamp()`. + * await updateDoc(documentRef, { + * 'aut.firstName': 'NewFirstName', + * lut: serverTimestamp() + * }); * - * const postSnap = await firebase.firestore() - * .collection('posts') - * .withConverter(postConverter) - * .doc().get(); - * const post = postSnap.data(); - * if (post !== undefined) { - * post.title; // string - * post.toString(); // Should be defined - * post.someNonExistentProperty; // TS error + * // The TypeScript compiler will fail to compile if the `data` argument to + * // `updateDoc()` is _not_ compatible with `WithFieldValue`. + * // This type checking prevents the caller from specifying objects with + * // incorrect properties or property values. + * // @ts-expect-error "Argument of type { title: string; } is not assignable + * // to parameter of type WithFieldValue" + * await updateDoc(documentRef, { title: 'New Title' }); + * const snapshot2: DocumentSnapshot = await getDoc(documentRef); + * const post2: Post = snapshot2.data()!; + * if (post2) { + * assertEqual(post2.title, 'My Life'); + * assertEqual(post2.author, 'NewFirstName Bar'); + * } * } * ``` */