You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何复制对象成员时保留readonly修饰符?TypeScript疑问

TypeScript MyReadonly2: Exclude 与 Omit 的差异及readonly修饰符保留方案

为什么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修饰符?

  1. 优先使用内置工具类型:Omit、Pick这类工具类型都是直接复用原类型的属性定义,能完整保留所有修饰符,避免手动映射带来的元数据丢失。
  2. 手动映射时显式保留修饰符(进阶):如果必须手动遍历键名,可以利用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 16:44:58