typescript的只读是编译期检查,不阻止运行时修改;需结合类型约束、开发者自觉与配套实践。readonlyarray仅保护数组结构,deepreadonly通过递归泛型实现深度只读,但无法处理循环引用。

不能靠单个 ReadonlyArray 或 DeepReadonly 就“彻底防止篡改”——TypeScript 的只读是编译期检查,不阻止运行时修改;真正起作用的是类型约束 + 开发者自觉 + 配套实践。但结合泛型,可以做到尽可能早地暴露错误、清晰表达意图、覆盖多层嵌套结构。
ReadonlyArray:数组层级的只读保护
ReadonlyArray<t></t> 是 TypeScript 内置的只读数组类型,它屏蔽了所有可变方法(push、pop、splice 等),只保留读取操作(map、filter、索引访问等)。
- 它不是运行时防护,而是类型层面的契约:一旦声明为
ReadonlyArray<string></string>,TS 就不允许你调用.push(),哪怕实际数组本身是可变的 - 适用于明确不希望函数修改数组本身(长度、顺序、元素引用)的场景,比如配置列表、枚举项、静态菜单
- 注意:它只保护数组结构,不递归保护内部对象属性。例如
ReadonlyArray中的name仍可被修改(除非该对象本身也定义为只读)
DeepReadonly:手动实现的深度只读泛型
TypeScript 没有内置 DeepReadonly,但可通过递归泛型自己定义:
type DeepReadonly<t> = T extends object
? {
readonly [K in keyof T]: DeepReadonly<t>;
}
: T;</t></t>
这个泛型会逐层把对象的每个属性标记为 readonly,包括嵌套对象、数组、元组等。
- 对数组,它会把
T[]转为ReadonlyArray<deepreadonly>></deepreadonly>,既禁用数组方法,又递归保护每个元素 - 对联合类型、
null、undefined、原始类型(string、number)直接返回原类型,不做额外包装 - 局限:无法处理循环引用(会导致类型无限展开),也不影响
Object.assign或直接赋值给非只读变量后的后续操作
泛型函数中统一接收只读数据
把 DeepReadonly 和函数参数泛型结合,让函数签名明确表达“我只读,不修改”:
function processData<t>(data: DeepReadonly<t>): void {
// 编译器会阻止你写 data.xxx = ... 或 data.arr.push(...)
console.log(data);
}</t></t>
- 调用时传入普通对象,TS 会自动推导并检查是否满足
DeepReadonly约束(即所有字段是否可被安全只读访问) - 若传入含可变方法的对象(如带
setX()方法的类实例),只要方法没被标记readonly,TS 不会报错——DeepReadonly只约束属性,不约束方法 - 更稳妥的做法是:让输入数据本身就在创建时就用只读结构(如
as const字面量、Object.freeze运行时冻结 + 类型断言)
配套建议:类型 + 实践双保险
光靠类型系统不够,需配合工程习惯:
- 对外暴露的 API 接口类型,优先使用
ReadonlyArray和DeepReadonly声明输入参数 - 内部状态管理(如 Redux store、React state)默认用只读类型建模,变更必须通过纯函数返回新对象
- 关键不可变数据(用户权限、配置项)在初始化时用
as const或Object.freeze(),再配合类型断言as DeepReadonly<typeof x></typeof> - CI 流程中开启
noImplicitAny、strict和exactOptionalPropertyTypes,强化只读检查效果











