如何复制对象成员时保留readonly修饰符?TypeScript疑问
为什么Exclude方案会丢失原有readonly修饰符?
先看两种常见实现的对比:
错误方案(Exclude + Pick)
type MyReadonly2_Error<T, K extends keyof T = keyof T> = { readonly [P in K]: T[P] } & { [P in Exclude<keyof T, K>]: T[P] }
这个实现里,Exclude<keyof T, K>只是提取了T中不属于K的键名集合,之后通过[P in ...]重新声明这些属性时,TypeScript会默认生成可变属性——不管原属性是否带有readonly修饰符。因为手动映射键名的过程相当于创建全新的属性定义,原属性的修饰符元数据被丢失了。
比如原类型:
type User = { readonly id: number; name: string; readonly age: number; }
使用错误方案后,MyReadonly2_Error<User, 'name'>的结果会把原本readonly的id和age变成可变属性,完全覆盖了原修饰符。
正确方案(Omit)
type MyReadonly2_Correct<T, K extends keyof T = keyof T> = { readonly [P in K]: T[P] } & Omit<T, K>
Omit的本质是Pick<T, Exclude<keyof T, K>>,但它是TypeScript内置的工具类型,核心区别在于:Omit直接复用原类型T中除K之外的所有属性定义,包括readonly、可选性等修饰符,相当于把原类型的对应部分“原封不动”拿过来,不会重新生成属性。
还是用上面的User类型,MyReadonly2_Correct<User, 'name'>会保留id和age的readonly修饰符,仅把name设为readonly。
关于K的类型约束差异
你提到两者仅K的类型约束不同,大概率是错误方案中K的约束没有严格限制为keyof T的子集(比如写成K extends string),但即使约束正确(K extends keyof T),Exclude+Pick的方式依然会丢失修饰符——这才是核心问题,约束差异只是表象,本质是属性声明方式的不同。
如何确保复制对象成员时保留原有readonly修饰符?
- 优先使用内置工具类型:Omit、Pick这类工具类型都是直接复用原类型的属性定义,能完整保留所有修饰符,避免手动映射带来的元数据丢失。
- 手动映射时显式保留修饰符(进阶):如果必须手动遍历键名,可以利用TypeScript 4.1+的映射修饰符控制,比如:
type PreserveModifiers<T> = { [P in keyof T]: T[P] } & { readonly [P in keyof T as IsReadonly<T, P> extends true ? P : never]: T[P] } // 辅助类型:判断属性是否为readonly type IsReadonly<T, P extends keyof T> = (<U>() => U extends { readonly [Q in P]: T[P] } ? 1 : 2) extends (<U>() => U extends { [Q in P]: T[P] } ? 1 : 2) ? false : true
不过这种方式比较繁琐,日常开发中用Omit/Pick足够解决问题。
内容的提问来源于stack exchange,提问作者Janice Zhong

