如何为Firestore嵌套集合创建TypeScript类型安全的getDocument函数
问题描述
我正在使用Firestore数据库,并已创建如下数据模型:
type FirestoreCollection<T> = { documentType: T; subcollections?: { [key: string]: FirestoreCollection<object>; }; }; type FirestoreSchema< T extends { [key: string]: U }, U = FirestoreCollection<object> > = T; export type FirestoreModel = FirestoreSchema<{ 'chat-messages': { documentType: ChatMessage; }; chats: { documentType: Chat; subcollections: { 'chat-participants': { documentType: ChatParticipant; }; }; }; }>;
该模型运行正常。我希望为一个函数创建类型定义,该函数接收一组参数,每个参数结构为{ collection: string, id: string },要求当前参数的collection必须是前一个参数对应集合的子集合,否则抛出类型错误。函数的预期行为如下:
// 合法:有效根级文档 getDocument( { collection: 'chat-messages', id: '123' } ); // 合法:'chat-participants'是'chats'的子集合 getDocument( { collection: 'chats', id: '123' }, { collection: 'chat-participants', id: '456' } ); // 错误:'chats'不是'chat-messages'的子集合 getDocument( { collection: 'chat-messages', id: '123' }, { collection: 'chats', id: '456' } ); // 错误:'invalid'不是根级集合 getDocument({ collection: 'invalid', id: '123' })
请教实现思路?
实现思路
核心是利用TypeScript的递归类型约束和条件类型,逐步验证每一层集合的父子关系,具体步骤如下:
1. 提取集合的子集合名称类型
先定义工具类型,从指定集合类型中提取它的所有子集合名称:
type GetSubcollections<C extends FirestoreCollection<object>> = C['subcollections'] extends infer S ? S extends { [key: string]: FirestoreCollection<object> } ? keyof S : never : never;
这个类型会返回目标集合的子集合名称联合类型,若没有子集合则返回never。
2. 递归构建合法路径链类型
定义递归类型,描述合法的集合路径链:
- 第一层参数必须是
FirestoreModel中的根集合 - 后续每一层参数的集合必须是前一层集合的子集合
// 单步路径的类型约束 type ValidPathStep<PrevCollection extends FirestoreCollection<object>> = { collection: GetSubcollections<PrevCollection>; id: string; }; // 区分根路径和子路径的类型 type ValidPath<Schema, CurrentCollection extends FirestoreCollection<object> = never> = CurrentCollection extends never ? { collection: keyof Schema; id: string } : ValidPathStep<CurrentCollection>; // 递归构建完整的路径参数链 type ValidPathChain<Schema, PrevCollection extends FirestoreCollection<object> = never> = | [ValidPath<Schema, PrevCollection>] | [ValidPath<Schema, PrevCollection>, ...ValidPathChain<Schema, Schema[ValidPath<Schema, PrevCollection>['collection']]>];
通过递归逻辑,让每一层参数的类型依赖于前一层选择的集合,实现链式的父子关系约束。
3. 绑定类型到函数
将路径链类型应用到函数参数上:
declare function getDocument<Schema extends FirestoreSchema<any>>( ...path: ValidPathChain<Schema> ): void; // 测试用例 getDocument<FirestoreModel>({ collection: 'chat-messages', id: '123' }); // 合法 getDocument<FirestoreModel>({ collection: 'chats', id: '123' }, { collection: 'chat-participants', id: '456' }); // 合法 getDocument<FirestoreModel>({ collection: 'chat-messages', id: '123' }, { collection: 'chats', id: '456' }); // 类型错误 getDocument<FirestoreModel>({ collection: 'invalid', id: '123' }); // 类型错误
优化:自动推导Schema类型
如果不想手动传入FirestoreModel,可以调整函数定义实现自动推导:
declare function getDocument< Schema extends FirestoreSchema<any>, Path extends ValidPathChain<Schema> >(...path: Path): void; // 使用时无需指定Schema getDocument({ collection: 'chat-messages', id: '123' }); // 合法
内容的提问来源于stack exchange,提问作者Ryan
相关产品推荐
相关产品推荐

