TypeScript中基于MGroup属性约束insertRelation的field参数
利用TypeScript类型系统限制MongoDB数据库层的参数冲突
我们正在为MongoDB构建数据库层,希望通过TypeScript的类型系统避免参数冲突。每个文档类型(MSite、MOrganization等)包含users和/或machines属性,需要实现API根据传入的MGroup类型,限制field参数的可选值。
具体需求示例:
MRegion包含users但不包含machines,传入match.type="region"时,field不能设为machinesMSite同时包含users和machines,两种field值都允许MSystem不包含这两个属性,传入该类型时field无合法值
当前函数实现如下:
export type MGroup = MSite | MOrganization | MRegion | MDistributor | MSystem export async function insertRelation< T extends MRelation >({ match, field, relation, }: { match: Filter<MGroup> & { type: string; _id: string } field: 'users' | 'machines' relation: Omit<T, AutomatedProperties> }): Promise<void> { ...function body... }
我尝试过条件类型和函数重载两种方案,但始终无法正确设置类型,函数内部会出现类型不一致的错误,恳请帮助。
以下是简化的类型定义和当前函数:
// 实际项目中Filter类型来自MongoDB,此处做简化 type Filter<T> = { } type MSite = { parent: string type: "site" users: Array<any> machines: Array<any> } type MOrganization = { parent: string type: "organization" users: Array<any> machines: Array<any> } type MRegion = { parent: string type: "region" users: Array<any> } type MDistributor = { parent: string type: "distributor" users: Array<any> } type MSystem = { type: "system" } export type MGroup = MSite | MOrganization | MRegion | MDistributor | MSystem export async function insertRelation({ match, field, }: { match: Filter<MGroup> & { type: string; _id: string } field: 'users' | 'machines' }): Promise<void> { } // 这段代码应该触发类型错误,但目前没有 await insertRelation({ match: { type: 'distributor', _id: 'test' }, field: 'machines' })
解决方案
方案1:函数重载+类型推导
为每个MGroup子类型定义明确的函数重载,绑定match.type与允许的field值:
// 定义每个类型允许的field集合 type AllowedFields<T> = T extends { users: any } ? 'users' : never | T extends { machines: any } ? 'machines' : never; // 为每个子类型定义重载 export async function insertRelation(match: Filter<MSite> & { type: "site"; _id: string }, field: AllowedFields<MSite>): Promise<void>; export async function insertRelation(match: Filter<MOrganization> & { type: "organization"; _id: string }, field: AllowedFields<MOrganization>): Promise<void>; export async function insertRelation(match: Filter<MRegion> & { type: "region"; _id: string }, field: AllowedFields<MRegion>): Promise<void>; export async function insertRelation(match: Filter<MDistributor> & { type: "distributor"; _id: string }, field: AllowedFields<MDistributor>): Promise<void>; export async function insertRelation(match: Filter<MSystem> & { type: "system"; _id: string }, field: never): Promise<void>; // 函数实现 export async function insertRelation({ match, field, }: { match: Filter<MGroup> & { type: string; _id: string } field: 'users' | 'machines' }): Promise<void> { // 内部通过类型守卫处理不同逻辑 switch(match.type) { case "site": case "organization": // 此处field为'users'|'machines' break; case "region": case "distributor": // 此处field仅为'users' break; case "system": throw new Error("MSystem不支持users或machines字段"); } } // 现在会触发类型错误:"machines"不能赋给"users"类型参数 await insertRelation({ match: { type: 'distributor', _id: 'test' }, field: 'machines' })
方案2:泛型约束+类型映射
通过类型映射绑定type字符串与对应子类型,再推导允许的field:
// 建立type到子类型的映射 type GroupTypeMap = { "site": MSite, "organization": MOrganization, "region": MRegion, "distributor": MDistributor, "system": MSystem } // 提取当前类型允许的field type AllowedFields<T> = keyof Pick<T, 'users' | 'machines'>; export async function insertRelation<K extends keyof GroupTypeMap>({ match, field, }: { match: Filter<GroupTypeMap[K]> & { type: K; _id: string } field: AllowedFields<GroupTypeMap[K]> }): Promise<void> { // 内部类型完全安全 if (match.type === "system") { throw new Error("MSystem无合法字段"); } } // 触发类型错误:"machines"不符合MDistributor的允许字段 await insertRelation({ match: { type: 'distributor', _id: 'test' }, field: 'machines' })
两种方案均可实现需求:
- 方案1直观易懂,适合类型数量较少的场景
- 方案2更简洁,新增
MGroup子类型时只需扩展GroupTypeMap即可
内容的提问来源于stack exchange,提问作者Tx3
相关产品推荐
相关产品推荐

