如何在@firebase/firestore与firebase-admin间共享FirestoreDataConverter?
问题背景
我正在开发一个使用Firestore的Angular应用,希望在Firebase Functions中复用Angular库定义的接口和FirestoreDataConverter。核心障碍是QueryDocumentSnapshot类在firebase-admin/firestore和@firebase/firestore中的实现存在差异。
我的尝试
为了兼容两个环境,我基于GitHub Issue的评论声明了联合类型:
import { QueryDocumentSnapshot as QueryDocumentSnapshotFire } from '@angular/fire/firestore'; import { QueryDocumentSnapshot as QueryDocumentSnapshotAdmin } from '@google-cloud/firestore'; export type QueryDocumentSnapshot = QueryDocumentSnapshotAdmin | QueryDocumentSnapshotFire;
接着用Zod定义了业务类型和转换器:
import { z } from 'zod'; import { FirestoreDataConverter } from '@angular/fire/firestore'; export type FirestoreUser = z.infer<typeof firestoreUserSchema>; export const firestoreUserSchema = z.object({ email: z.string(), }); export type FirestoreUserDb = z.infer<typeof firestoreUserDbSchema>; export const firestoreUserDbSchema = z.object({ email: z.string(), }); export const firestoreUserConverter: FirestoreDataConverter<FirestoreUser, FirestoreUserDb> = { fromFirestore: (snapshot: QueryDocumentSnapshot): FirestoreUser => { const validFirestoreUserDb = firestoreUserDbSchema.parse(snapshot.data()); return { email: validFirestoreUserDb.email }; }, toFirestore: (firestoreUser: FirestoreUser): FirestoreUserDb => { const validFirestoreUser = firestoreUserSchema.parse(firestoreUser); return { email: validFirestoreUser.email }; }, };
不同环境的表现
前端(Angular)
使用转换器时,DocumentReference类型能正确自动推断:
const userDocRef = doc(this.firestore, FirestoreCollections.Users, uid).withConverter(firestoreUserConverter);
userDocRef的类型为:
const userDocRef: DocumentReference<{ email: string; }, { email: string; }>
Firebase Functions(Admin)
调用set()时类型无法自动推断,传入不符合类型的值(比如null)也不会报错:
const result = await db.collection(FirestoreCollections.Users).doc(user.uid).withConverter(firestoreUserConverter).set({ email: null, // 此处应仅接受string类型,但无类型提示 });
必须显式指定泛型参数,才能获得正确的类型检查:
const result = await db.collection(FirestoreCollections.Users) .withConverter<FirestoreUser, FirestoreUserDb>(firestoreUserConverter) .doc(user.uid) .set({ email: user.email });
显式指定后,DocumentReference类型才变为:
FirebaseFirestore.DocumentReference<{ email: string; }, { email: string; }>
未指定泛型时,类型默认为:
FirebaseFirestore.DocumentReference<unknown, FirebaseFirestore.DocumentData>
疑问与解答
1. 为何调用set()时类型无法正确推断?
这是因为firebase-admin依赖的@google-cloud/firestore与前端@angular/fire依赖的@firebase/firestore,二者的FirestoreDataConverter属于不同的类型空间。你的转换器声明使用的是前端版本的FirestoreDataConverter,在Admin环境下TypeScript无法自动将其与Admin版本的转换器类型兼容,因此无法推导泛型参数。
联合类型QueryDocumentSnapshot只能解决快照类型的兼容,但无法抹平两个SDK在转换器接口上的类型差异,导致自动推断失效。
2. 有没有更优方案实现前后端FirestoreDataConverter共享?我不希望在库中导入@google-cloud/firestore。
可以通过类型隔离+适配器模式实现,彻底消除共享库对Firestore SDK的依赖:
步骤1:定义环境无关的共享逻辑
在共享库中只保留纯业务类型和转换函数,不引入任何Firestore SDK类型:
// src/shared/firestore/user-converter.ts import { z } from 'zod'; // 纯业务类型 export type FirestoreUser = z.infer<typeof firestoreUserSchema>; export const firestoreUserSchema = z.object({ email: z.string() }); export type FirestoreUserDb = z.infer<typeof firestoreUserDbSchema>; export const firestoreUserDbSchema = z.object({ email: z.string() }); // 纯转换逻辑,只处理数据结构 export const userFromFirestore = (data: FirestoreUserDb): FirestoreUser => { const validData = firestoreUserDbSchema.parse(data); return { email: validData.email }; }; export const userToFirestore = (user: FirestoreUser): FirestoreUserDb => { const validUser = firestoreUserSchema.parse(user); return { email: validUser.email }; };
步骤2:在前端和Admin中分别适配
- 前端(Angular):
// 前端代码 import { FirestoreDataConverter } from '@angular/fire/firestore'; import { userFromFirestore, userToFirestore, FirestoreUser, FirestoreUserDb } from '../shared/firestore/user-converter'; export const firestoreUserConverter: FirestoreDataConverter<FirestoreUser, FirestoreUserDb> = { fromFirestore: (snapshot) => userFromFirestore(snapshot.data()), toFirestore: (user) => userToFirestore(user), };
- Firebase Functions(Admin):
// Admin代码 import { FirestoreDataConverter } from 'firebase-admin/firestore'; import { userFromFirestore, userToFirestore, FirestoreUser, FirestoreUserDb } from '../shared/firestore/user-converter'; export const firestoreUserConverter: FirestoreDataConverter<FirestoreUser, FirestoreUserDb> = { fromFirestore: (snapshot) => userFromFirestore(snapshot.data()), toFirestore: (user) => userToFirestore(user), };
这种方案下,共享库完全独立于Firebase SDK,前端和Admin各自适配对应SDK的转换器接口,TypeScript能完美自动推导泛型,无需手动指定。
3. 是否应彻底更换方案,使用Typesaurus这类工具?
这取决于你的项目阶段和需求:
- 如果项目处于初期,且对Firestore类型安全有极高要求,Typesaurus是不错的选择——它通过统一的类型系统封装了Firestore操作,天然支持前后端共享类型,不需要手动维护转换器,开发体验更流畅。
- 如果项目已有一定规模,或依赖Angular Fire的现有集成,则没必要彻底切换。前面的适配器方案改动成本低,能快速解决共享问题,同时保留现有技术栈。
内容的提问来源于stack exchange,提问作者Igor Augustyński

