如何对Firestore.FieldValue做类型收窄,实现类型安全的Firestore封装
解决方案
你当前的临时方案可以正常运行,但暴力强转完全可以避免,核心思路是用TS的联合类型+操作符重载,让写入操作的类型自动兼容FieldValue,同时读取操作的类型不会包含FieldValue相关定义。
第一步:拆分读写类型
首先将你的数据模型拆分为读类型和写类型,读类型就是你原本定义的纯业务数据类型,完全不含FieldValue;写类型则递归将每个字段的类型替换为 原始类型 | FieldValue,针对不同FieldValue方法做精准的类型约束:
// 先定义所有支持的FieldValue类型 type FieldValue = | ReturnType<typeof firestore.FieldValue.arrayUnion> | ReturnType<typeof firestore.FieldValue.arrayRemove> | ReturnType<typeof firestore.FieldValue.increment> | ReturnType<typeof firestore.FieldValue.serverTimestamp> | ReturnType<typeof firestore.FieldValue.delete> // 递归生成兼容FieldValue的写类型 type WithFieldValue<T> = T extends (infer U)[] ? T | ReturnType<typeof firestore.FieldValue.arrayUnion<U>> | ReturnType<typeof firestore.FieldValue.arrayRemove<U>> : T extends number ? T | ReturnType<typeof firestore.FieldValue.increment> : T extends Date ? T | ReturnType<typeof firestore.FieldValue.serverTimestamp> : T extends object ? { [K in keyof T]: WithFieldValue<T[K]> } | ReturnType<typeof firestore.FieldValue.delete> : T | ReturnType<typeof firestore.FieldValue.delete>
第二步:操作方法入参绑定写类型
所有写入类操作(set、update、create)的入参用WithFieldValue<你的数据类型>,读取操作(get、snapshot监听)的返回值用原本的纯业务类型即可。这样你在写入的时候可以直接传原生FieldValue方法的返回值,不需要额外封装辅助函数,TS会自动做类型校验。
比如你有如下业务数据类型定义:
interface User { name: string age: number tags: string[] updateTime: Date }
那么update操作的入参类型就是Partial<WithFieldValue<User>>,你写下面的代码完全不会报错,还会有严格的类型校验:
db.collection('users').doc('1').update({ age: firestore.FieldValue.increment(1), // 自动校验increment的入参必须是数字 tags: firestore.FieldValue.arrayUnion('newTag'), // 自动校验arrayUnion的入参必须是string updateTime: firestore.FieldValue.serverTimestamp() })
第三步:可选的工具函数封装
如果不想让库的使用者直接引入Firestore的FieldValue,你可以封装和你现在类似的工具函数,但不需要暴力强转,直接指定返回类型为对应的FieldValue联合类型即可:
const arrayUnion = <T>(...elements: T[]) => { return firestore.FieldValue.arrayUnion(...elements) as WithFieldValue<T[]> } const increment = (n: number) => { return firestore.FieldValue.increment(n) as WithFieldValue<number> } const serverTimestamp = () => { return firestore.FieldValue.serverTimestamp() as WithFieldValue<Date> }
这个方案比你当前的强转更安全,因为不会出现把FieldValue误用到读类型场景的问题,读写类型拆分后读侧的类型还是100%纯业务类型,不会混入任何Firestore相关的类型定义。
如果涉及serverTimestamp的特殊场景,需要支持写入时传标记、读取后是Date的逻辑,你可以单独对Date字段的写类型做特殊处理,不会影响读侧的类型判断。
内容的提问来源于stack exchange,提问作者Acid Coder

