Firebase Admin SDK调用withConverter报类型错误 客户端正常
客户端@firebase/firestore(Web JS SDK)和服务端firebase-admin(Admin Node SDK)是两套完全独立的Firestore实现,二者的FirestoreDataConverter、QueryDocumentSnapshot、SnapshotOptions、PartialWithFieldValue、DocumentData等类型都来自各自独立的类型声明包,并不通用。
你当前的converter显式绑定了客户端SDK的类型,直接传给Admin SDK的withConverter方法时,TS会校验出类型结构不匹配抛出错误。这类跨包同名类型的不匹配,TS通常会输出超长的结构差异提示,不会明确说明类型来源不同,所以错误信息看起来非常模糊,难以直接定位。
converter本身的运行时逻辑没有问题,只是类型声明不兼容双端。
根据你对类型校验严格度的需求,二选一即可:
方案1:宽松类型适配(代码量最少)
去掉converter上绑定的单端强类型声明,把参数类型放宽到兼容双端结构,运行时逻辑完全不需要改动,即可同时在两端使用。
调整后的converter代码:
// 仅导入可共用的业务类型,不要导入绑定单端的FirestoreDataConverter等类型 import type { User } from './your-user-type-path'; const userConverter = { toFirestore(data: any): DocumentData { const user: Record<string, any> = {}; if ('id' in data) { user.id = data.id; } if ('email' in data) { user.email = data.email; } if ('displayName' in data) { user.displayName = data.displayName; } if ('color' in data) { user.color = data.color; } if ('avatarUrl' in data) { user.avatarUrl = data.avatarUrl; } if ('status' in data) { user.status = data.status; } if ('jobTitle' in data) { user.jobTitle = data.jobTitle; } if ('phoneNumber' in data) { user.phoneNumber = data.phoneNumber; } if ('providerIds' in data) { user.providerIds = data.providerIds; } return user; }, fromFirestore: (snap: any, options?: any): User => { const data = snap.data(options); return { id: snap.id, color: data.color, email: data.email, status: data.status, jobTitle: data.jobTitle, avatarUrl: data.avatarUrl, displayName: data.displayName, phoneNumber: data.phoneNumber, providerIds: data.providerIds, }; }, };
这个版本的converter可以直接传给客户端和Admin SDK的withConverter方法,运行时行为和你原来的实现完全一致。
方案2:工厂函数适配(保留双端强类型校验)
如果你不想放弃TS的类型校验,可以封装一个通用的converter工厂函数,分别传入两端SDK的对应类型,生成适配各自环境的converter实例,核心逻辑只需要写一次,同时保留双端的完整类型提示。
实现代码:
import type { User } from './your-user-type-path'; import * as clientDb from '@firebase/firestore'; import * as adminDb from 'firebase-admin/firestore'; // 核心转换逻辑封装,复用所有字段处理代码 function buildUserConverter<TTypes extends { FirestoreDataConverter: any; QueryDocumentSnapshot: any; SnapshotOptions: any; PartialWithFieldValue: any; DocumentData: any; }>(sdkTypes: TTypes): TTypes['FirestoreDataConverter']<User> { return { toFirestore(data: TTypes['PartialWithFieldValue']<User>): TTypes['DocumentData'] { const user: Record<string, any> = {}; // 字段处理逻辑和原实现完全一致 if ('id' in data) user.id = data.id; if ('email' in data) user.email = data.email; if ('displayName' in data) user.displayName = data.displayName; if ('color' in data) user.color = data.color; if ('avatarUrl' in data) user.avatarUrl = data.avatarUrl; if ('status' in data) user.status = data.status; if ('jobTitle' in data) user.jobTitle = data.jobTitle; if ('phoneNumber' in data) user.phoneNumber = data.phoneNumber; if ('providerIds' in data) user.providerIds = data.providerIds; return user; }, fromFirestore( snap: TTypes['QueryDocumentSnapshot'], options: TTypes['SnapshotOptions'] ): User { const data = snap.data(options); return { id: snap.id, color: data.color, email: data.email, status: data.status, jobTitle: data.jobTitle, avatarUrl: data.avatarUrl, displayName: data.displayName, phoneNumber: data.phoneNumber, providerIds: data.providerIds, }; } } } // 生成客户端专用converter const clientUserConverter = buildUserConverter({ FirestoreDataConverter: clientDb.FirestoreDataConverter, QueryDocumentSnapshot: clientDb.QueryDocumentSnapshot, SnapshotOptions: clientDb.SnapshotOptions, PartialWithFieldValue: clientDb.PartialWithFieldValue, DocumentData: clientDb.DocumentData, }); // 生成Admin SDK专用converter const adminUserConverter = buildUserConverter({ FirestoreDataConverter: adminDb.FirestoreDataConverter, QueryDocumentSnapshot: adminDb.QueryDocumentSnapshot, SnapshotOptions: adminDb.SnapshotOptions, PartialWithFieldValue: adminDb.PartialWithFieldValue, DocumentData: adminDb.DocumentData, });
使用时,客户端代码用clientUserConverter,服务端代码用adminUserConverter即可,两边都有完整的类型校验,后续修改字段逻辑只需要改工厂函数里的一处代码。
- 不要尝试用交叉类型、
as类型断言强行把客户端converter直接传给Admin SDK,两个SDK的类型是独立维护的,版本升级时类型结构可能发生变化,强行断言会埋下隐性的兼容问题。
内容的提问来源于stack exchange,提问作者dlarroder

