如何用TypeScript递归类型实现对象字段的递进必选约束?
TypeScript递进约束类型实现问题
我定义了如下TypeScript类型:
type WeakGuaranteeType = { a?: string, b?: string, c?: string, d?: string, }
我希望实现更强的类型约束:当b存在时a必须存在,当c存在时b必须存在,以此类推。即:
{ a: "" } // 合法 { a: "", b: "" } // 合法 { a: "", b: "", c: "" } // 合法 { c: "" } // 不合法!!
是否可以用TypeScript描述这种类型?
我的首次尝试如下:
type BetterType = | {} | ({a: string} & ( | {} | ({b: string} & ( | {} | ({c: string} & ( // 依此类推... )) )) ))
该方案存在两个问题:
- 写法冗长,需手动编写;
- 会触发“Expression produces a union type that is too complex to represent”错误。
我认为递归方案更合适,于是编写了如下代码:
type RecursiveRequire<Keys extends string[]> = Keys extends [] // 检查Keys是否为空 ? {} // 基准情况:若Keys为空,返回{} : ( // 递归情况:添加第一个键并对剩余键递归 { [Key in Keys[0]]: string } & RecursiveRequire</* 此处应如何表示“Keys中除第一个外的所有元素”? */> ); type BestType = RecursiveRequire<["a", "b", "c", "d"]>;
请问如何完善这个递归类型实现?
解决方案
首先,解决“Keys中除第一个外的所有元素”的问题,可以用TypeScript的数组解构推断:Keys extends [infer First extends string, ...infer Rest extends string[]],其中Rest就是除第一个元素外的剩余元素数组。
同时需要调整递归逻辑——原写法会生成所有键都必填的交叉类型,而我们需要的是“递进可选的联合类型”(允许停在任意步骤,但前面的键必须存在)。正确的递归实现如下:
type RecursiveRequire<Keys extends string[]> = // 基准情况:空数组返回空对象 Keys extends [] ? {} // 解构数组,提取第一个元素First和剩余元素Rest : Keys extends [infer First extends string, ...infer Rest extends string[]] ? // 两种情况的联合: // 1. 空对象(允许不选任何键) {} | // 2. 必须包含当前First键,同时递归约束剩余键的递进规则 ({ [K in First]: string } & RecursiveRequire<Rest>) : never; // 测试类型 type BestType = RecursiveRequire<["a", "b", "c", "d"]>; // 合法示例 const valid1: BestType = {}; const valid2: BestType = { a: "" }; const valid3: BestType = { a: "", b: "" }; const valid4: BestType = { a: "", b: "", c: "" }; const valid5: BestType = { a: "", b: "", c: "", d: "" }; // 不合法示例(TypeScript会报错) const invalid1: BestType = { b: "" }; // 缺少a,报错 const invalid2: BestType = { c: "" }; // 缺少a和b,报错 const invalid3: BestType = { a: "", c: "" }; // 缺少b,报错
核心逻辑说明
- 每一层递归都会生成两种可选类型:要么不选当前层级及以后的所有键(空对象),要么必须选当前键,同时遵守剩余键的递进约束。
- 通过递归展开,最终会生成所有符合要求的递进类型联合:
{} | {a:string} | {a:string,b:string} | {a:string,b:string,c:string} | {a:string,b:string,c:string,d:string}
可选调整:排除空对象情况
如果你想只允许至少包含一个键的合法组合,可以去掉联合中的{},修改为:
type RecursiveRequireNonEmpty<Keys extends string[]> = Keys extends [] ? never : Keys extends [infer First extends string, ...infer Rest extends string[]] ? ({ [K in First]: string } & ({} | RecursiveRequireNonEmpty<Rest>)) : never;
内容的提问来源于stack exchange,提问作者QuinnF
相关产品推荐
相关产品推荐

