TypeScript 嵌套属性场景下互斥类型校验失效问题
嵌套场景下互斥类型校验失效问题
注:本问题不属于现有通用互斥类型方案的覆盖范围,多层嵌套属性的互斥需求超出了常规“n个候选属性仅存其一”逻辑的适配场景,现有公开搜索结果均未覆盖该特定场景。
项目需求背景
项目对接结构不统一的后端JSON HAL文档,需要编写结构归一化函数normalizeRepresentation(),并通过强类型约束校验输入结构的合法性。
核心校验规则:
- 实体数据必须且仅能出现在5个位置之一:根文档节点、
document属性、data属性、item属性、item.data属性 - 可选的
contentType属性仅能出现在根文档节点或实体数据内部,两个位置不能同时存在 - 目标函数签名为:
function normalizeRepresentation<T = any>(representation: IHalRepresentation<T>): IHalDocument<T>;
合法输入示例(以实体类型{ id: number }为例)
const valid: IHalRepresentation<{ id: number }>[] = [ { id: 1 }, { id: 1, contentType: 'foo' }, { data: { id: 1 }, contentType: 'foo' }, { document: { id: 1, contentType: 'foo' } }, { item: { data: { id: 1, contentType: 'foo' } } }, { item: { data: { id: 1 } }, contentType: 'foo' }, ];
原有实现存在的校验漏洞
目前类型实现可正确拦截大部分无效结构,但在item.data嵌套承载实体的场景下,以下三类本应报错的结构可正常通过编译:
interface Doc { id: number } // 错误:根节点与实体内部同时存在contentType const failureItemDataWithTwoContentTypes: IHalRepresentation<Doc> = { item: { data: { id: 1, contentType: 'bar' } }, contentType: 'foo', }; // 错误:contentType错误放置在item层级 const failureItemDataWithContentTypeOnItem: IHalRepresentation<Doc> = { item: { data: { id: 1 }, contentType: 'bar' }, }; // 错误:item内部同时存在平铺实体字段与嵌套data实体 const failureItemandItemData: IHalRepresentation<Doc> = { item: { id: 1, data: { id: 2 } }, };
问题根源
原有类型逻辑的缺陷集中在嵌套场景的约束覆盖不全:
- 原有
WithContentType工具类型仅支持同级属性的contentType互斥,无法覆盖“item.data内部的contentType与根节点contentType跨层互斥”的需求 item.data分支的类型定义中,未对item层级的属性做多余值校验,导致item可以任意添加错误位置的contentType、平铺的实体字段- 根节点的互斥约束未覆盖
item.data分支,导致根节点的contentType不会和实体内部的contentType做重复校验
修复实现
调整工具类型的约束逻辑,补全跨层互斥与多余属性校验:
// 工具类型:生成严格的空属性约束 type NeverProps<T> = { [K in keyof T]?: never }; // 调整后的WithContentType:支持指定互斥层级的contentType规则 type WithContentType<T, CTInEntity extends boolean> = CTInEntity extends true // contentType在实体内部的场景:实体可带contentType,外层不允许出现contentType ? T & { contentType?: string } & NeverProps<{ contentType: never }> // contentType在实体外部的场景:实体不允许带contentType,外层保留contentType可选位 : T & NeverProps<{ contentType: never }> & { contentType?: string }; // 可承载实体的根属性列表 type ItemProperties = 'data' | 'item' | 'document'; // 调整后的Content类型:补全嵌套场景的全链路约束 type Content< T, P extends ItemProperties | null, CTLocation: 'root' | 'entity' | null = null > = // 实体在根节点的场景 P extends null ? T & { contentType?: string } & NeverProps<Record<ItemProperties, any>> : CTLocation extends 'entity' // 实体在指定P属性内、contentType在实体内部的场景 ? { [K in P]: WithContentType<T, true> } & NeverProps<{ contentType: string }> & NeverProps<Record<Exclude<ItemProperties, P>, any>> : CTLocation extends 'root' // 实体在指定P属性内、contentType在根节点的场景 ? { [K in P]: WithContentType<T, false> } & { contentType?: string } & NeverProps<Record<Exclude<ItemProperties, P>, any>> // 非嵌套的单层承载场景:兼容原有逻辑 : { [K in P]: T } & NeverProps<Record<Exclude<ItemProperties, P>, keyof T>> & { contentType?: string }; // 调整后的IHalRepresentation:覆盖所有5种合法实体位置 type IHalRepresentation<T extends Record<string, any>> = // 实体在根节点 | Content<T, null> // 实体在data属性 | Content<T, 'data', 'entity'> | Content<T, 'data', 'root'> // 实体在document属性 | Content<T, 'document', 'entity'> | Content<T, 'document', 'root'> // 实体在item属性(平铺场景) | Content<T, 'item', 'entity'> | Content<T, 'item', 'root'> // 实体在item.data属性(嵌套场景) | Content<{ data: T }, 'item', 'entity'> | Content<{ data: T }, 'item', 'root'>;
修复后,上述三类无效结构会被编译器准确识别报错,所有合法结构均可正常通过类型校验。
内容的提问来源于stack exchange,提问作者dx_over_dt
相关产品推荐
相关产品推荐

