# 34857-DefinedPartial
# 题目描述
实现 DefinedPartial<T>,把对象类型 T 展开成"所有非空子集对象"的联合:
type A = DefinedPartial<{ a: string; b: string }>;
// { a: string }
// | { b: string }
// | { a: string; b: string }
type B = DefinedPartial<Record<'a' | 'b' | 'c', string>>;
// { a: string }
// | { b: string }
// | { c: string }
// | { a: string; b: string }
// | { a: string; c: string }
// | { b: string; c: string }
// | { a: string; b: string; c: string }
对 N 个 key 的对象,结果恰好 2^N - 1 支。
# 分析
题目等价于"列出 T 所有非空 key 子集,并把每个子集用 Pick<T, subset> 还原成对应的对象"。
直觉做法是"对 keyof T 分发 + 递归挑另一个",但这里有个坑:条件类型分发时,K 变量会被替换成"当前单支",Exclude<K, X> 直接拿不到别的 key——递归只产出单 key 的 Pick,没法拼出双 key、三 key 的组合。
正确套路分两步:
- 先把
keyof T的联合转成元组——元组不参与分发,能在递归里稳定地"一个一个走"。 - 对元组递归,每一位有两种选择:跳过 当前 key,或把它加入累加集合
Acc。递归到底,Acc不为空就返回Pick<T, Acc>。2^N条路径天然覆盖所有子集,空集路径被兜底成never丢掉。
第 1 步需要"联合转元组"这个 hard 技巧,见 hard/730 联合转元组。
# 题解
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I,
) => void
? I
: never;
type LastOfUnion<U> = UnionToIntersection<
U extends any ? (x: U) => void : never
> extends (x: infer L) => void
? L
: never;
type UnionToTuple<U, Last = LastOfUnion<U>> = [U] extends [never]
? []
: [...UnionToTuple<Exclude<U, Last>>, Last];
type SubsetsFromKeys<
T,
Keys extends unknown[],
Acc extends keyof T,
> = Keys extends [infer F extends keyof T, ...infer Rest extends (keyof T)[]]
? SubsetsFromKeys<T, Rest, Acc> | SubsetsFromKeys<T, Rest, Acc | F>
: [Acc] extends [never]
? never
: Pick<T, Acc>;
type DefinedPartial<T> = SubsetsFromKeys<T, UnionToTuple<keyof T>, never>;
解读:
UnionToTuple<U>把keyof T的联合变成元组。它靠LastOfUnion用逆变一次抠一个末项,再递归处理剩下的。SubsetsFromKeys按元组一位一位递归:对当前位F,并集里同时展开两种选择——不加入(Acc不变)和加入(Acc | F)。由于元组递归不触发分发,Acc能稳定地累积成一个真正的 key 联合。- 递归出口:元组走完,
Acc若为never表示"一路上啥也没选"——对应空子集,不要它;否则用Pick<T, Acc>把真正累积到的 key 合集还原成对象。 - 相比常见的 "
Pick<T, X> & Pick<T, Y>" 式拼接,Pick<T, X | Y>在Equal下才是同一个类型,不需要额外Merge拍平——这是为什么要先转元组再累积 key 而不是直接交叉。
# 验证
type R1 = DefinedPartial<{ a: string; b: string }>;
// { a: string } | { b: string } | { a: string; b: string }
type R2 = DefinedPartial<Record<'a' | 'b' | 'c', string>>;
// 7 支
type R3 = DefinedPartial<Record<'a', number>>;
// { a: number }
# 知识点
- 枚举所有子集 = 对每个位置"选 / 不选":这是最经典的组合结构,与 21106 CombinationKeyType 的配对联合同属于 排列组合大乱炖 里的分发模型。
- 累积 key 要用
Acc | F(联合)而不是Pick<T, X> & Pick<T, Y>(交叉)。后者虽然语义相同,但会在Equal下被判为不等;前者统一走Pick<T, Acc>,结构扁平,见 对象遍历的 as 和索引访问。 - 为了让递归不被联合分发打断,先通过
UnionToTuple把keyof T转成元组,这样累加集合Acc才能稳定增长。UnionToTuple的核心是利用逆变推断一次抠一个末项,见 hard/730 联合转元组。