如何为克隆方法正确声明TypeScript的RecursiveOmit与RecursivePick?
问题分析与修正
问题描述
试图为基于JSON.parse(JSON.stringify(obj, ['myProperty']))的克隆方法实现RecursivePick和RecursiveOmit类型,但类型校验不符合预期:
- 调用
clone(c, ['b'])后,cc.b.a、cc.a.c本应不存在却未触发错误 - 调用
cloneWithBlackList(c, ['b'])后,cb.b.b本应不存在却未触发错误
问题根源
类型定义逻辑错误:
原代码中仅将不符合条件的属性类型设为never,但TypeScript不会自动移除never类型的属性,只会在赋值时报类型不匹配,而非提示属性不存在。正确的递归Pick/Omit需要完全移除不符合条件的属性,而非修改其类型。递归处理的范围偏差:
原类型对所有属性都进行递归处理,包括被标记为never的属性,导致类型系统仍认为这些属性存在。
修正后的代码
// 提取对象所有层级的key,用于约束传入的属性列表 type RecursiveKey<T> = T extends object ? keyof T | RecursiveKey<T[keyof T]> : never; // 递归Pick:只保留所有层级中属于K的属性 type RecursivePick<T, K extends PropertyKey> = T extends object ? { [P in keyof T as P extends K ? P : never]: T[P] extends object ? RecursivePick<T[P], K> : T[P] } : T; // 递归Omit:移除所有层级中属于K的属性 type RecursiveOmit<T, K extends PropertyKey> = T extends object ? { [P in keyof T as P extends K ? never : P]: T[P] extends object ? RecursiveOmit<T[P], K> : T[P] } : T; const clone = <T, K extends RecursiveKey<T>>(object: T, whiteListedProperties: K[]): RecursivePick<T, K> => { return JSON.parse(JSON.stringify(object, whiteListedProperties as (string | number)[])); }; const cloneWithBlackList = <T, K extends RecursiveKey<T>>(object: T, blackListedProperties: K[]): RecursiveOmit<T, K> => { return JSON.parse(JSON.stringify(object, (key: string, value: any): any => blackListedProperties.includes(key as K) ? undefined : value )); }; // 测试用例 const c = { a: { a: 1, b: 2, c: 3 }, b: { a: 1, b: 2 } }; const cc = clone(c, ['b']); cc.b.a = 2; // ✅ 报错:属性'a'不存在 cc.b.b = 2; // ✅ 正常 cc.a.c = 2; // ✅ 报错:属性'c'不存在 const cb = cloneWithBlackList(c, ['b']); cb.a.a = 2; // ✅ 正常 cb.b.b = 3; // ✅ 报错:属性'b'不存在 cb.a.c = 2; // ✅ 正常
关键修正点
- 使用映射类型的条件过滤(
as子句):通过P extends K ? P : never直接过滤掉不需要的属性,让类型系统认为这些属性不存在,而非仅修改其类型为never。 - 递归逻辑仅针对保留下来的属性执行,确保所有层级的属性都严格遵循Pick/Omit规则。
内容的提问来源于stack exchange,提问作者Darkwander
相关产品推荐
相关产品推荐

