TypeScript 4.5+中MyReadonly2交叉类型实现失效原因排查
问题背景
我在完成type-challenges中的类型题目,题目要求实现接收两个类型参数T、K的泛型MyReadonly2<T, K>,规则如下:
- K用于指定T中需要设置为只读的属性集合
- 若未传入K,则需要将T的所有属性设为只读,与内置
Readonly类型的行为一致
我编写的第一版方案在TypeScript 4.4版本可正常运行,但在4.5及以上版本中无法正常工作,代码如下:
//solution 1 type MyReadonly2<T, K extends keyof T = keyof T> = T & { readonly [P in K]: T[P] }
我找到的修复方案可在v4.7.2版本正常运行,代码如下:
// solution 2 type MyReadonly2<T, K extends keyof T = keyof T> = Omit<T, K> & { readonly [P in K]: T[P] }
我始终无法理解第一种实现方案失效的根本原因,需要技术层面的解答。
失效原因说明
TypeScript 4.5 交叉类型修饰符合并规则变更
第一版方案失效的核心原因是TypeScript 4.5对交叉类型的属性修饰符(包括readonly、可选修饰符?)合并逻辑做了破坏性更新:
- 4.4及更早版本中,交叉类型内如果同名属性出现在多个成员类型中,后定义的成员的属性修饰符会覆盖先定义的。第一版写法中,原始类型
T的K集合属性默认是可写状态,后面交叉的映射类型给K集合属性加了readonly修饰符,按照旧规则后者会覆盖前者,最终输出符合预期。 - 4.5及之后版本,TS调整了合并优先级:直接来自原始类型的属性,其自带的修饰符优先级高于交叉时追加的其他同名属性的修饰符。第一版直接交叉原始类型
T,K集合属性在原始T中是可写的,后续追加的readonly修饰符会被TS直接忽略,自然无法实现只读效果。
修复方案生效逻辑
第二版方案通过Omit<T, K>先把要修改修饰符的K集合属性从原始T中完全剔除,再交叉追加了readonly修饰符的K属性映射:
- 交叉的两个成员不存在同名属性:
Omit<T, K>仅保留T中不在K范围内的属性,原有修饰符完全不变 - 后续的映射类型仅包含K范围内的属性,不存在和原始T的同名属性修饰符冲突,
readonly修饰符可以被正确识别保留,最终输出符合预期。
注意:这次规则变更是TS为了修复此前交叉类型修饰符合并逻辑不一致的一系列问题推出的,从4.5版本开始,直接交叉原始类型追加修饰符的写法不再可靠。所有需要修改原类型部分属性修饰符的场景,都应该先通过
Omit等工具类型把待修改属性从原类型中移除,再交叉修改完修饰符的对应属性集合。
内容的提问来源于stack exchange,提问作者qian xiang
相关产品推荐
相关产品推荐

