TypeScript中Readonly类型可赋值给可变类型的原因及强不可变性实现方案咨询
我完全懂你的困惑——本来以为用Readonly<Todo>标记的实例应该是完全不可修改的,结果却能通过赋值给普通Todo类型绕开限制,这确实有点违背我们对“不可变性”的预期。咱们先搞清楚为什么会这样,再看看怎么解决这个问题:
为什么Readonly<T>能赋值给可变类型?
TypeScript采用的是结构类型系统,简单来说就是只要两个类型的结构(属性、方法)一致,就认为它们是兼容的。Readonly<T>本质上只是给T的所有属性加上了readonly的类型标记,但并没有改变对象本身的结构——它是个编译时的类型约束,不是运行时的强制限制。所以TypeScript会认为Readonly<Todo>和Todo是兼容的,允许互相赋值,这就给了我们绕过只读限制的机会。
实现强不可变性的几种方案
如果想要真正锁住对象,不让它被修改,可以试试下面这些方法:
1. 从类设计根源上做不可变约束
直接把类的属性设为readonly,或者用私有属性+只读getter的方式,让实例在运行时也无法被修改:
class ImmutableTodo { constructor( private readonly _name: string, private readonly _active = true, private readonly _completed = false, private readonly _editing = false ) {} // 只提供只读的访问器,不暴露setter get name() { return this._name; } get active() { return this._active; } get completed() { return this._completed; } get editing() { return this._editing; } toString() { return this._name; } // 如果需要修改属性,返回一个新的实例(不可变数据模式) withName(newName: string): ImmutableTodo { return new ImmutableTodo(newName, this._active, this._completed, this._editing); } }
这种方式下,不管是类型层面还是运行时,原有实例的属性都无法被修改,所有“修改”操作都会生成新对象,彻底保证不可变性。
2. 用自定义类型阻止只读类型向可变类型赋值
你可以定义一个Mutable工具类型,再配合类型守卫来限制赋值操作,虽然不能完全杜绝强制类型断言,但能在编译时增加一层约束:
// 移除类型的readonly修饰符 type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // 自定义类型守卫,判断是否是可变类型实例 function isMutableTodo(t: Readonly<Todo> | Todo): t is Mutable<Todo> { // 这里可以根据实际需求加运行时检查,比如判断实例是否有特定标记 return Object.prototype.hasOwnProperty.call(t, '_isMutable'); } function f() { let ro: Readonly<Todo> = new Todo("bugs bunny"); let t: Todo; if (isMutableTodo(ro)) { t = ro; } else { throw new Error("禁止将只读Todo实例赋值给可变类型"); } }
3. 使用不可变数据工具库(比如Immer)
如果你的场景是复杂状态管理,可以借助像Immer这样的库,它能帮你以“可变”的写法生成不可变的新对象,原对象始终保持不可变:
import { produce } from "immer"; let ro: Readonly<Todo> = new Todo("bugs bunny"); // 用produce生成新对象,原ro不会被修改 const updatedTodo = produce(ro, draft => { draft.name = "duffy duck"; });
总结
Readonly<T>更多是编译时的“提醒”,而非强制限制。如果想要真正的强不可变性,最好从类的设计入手(比如只读属性、不可变实例模式),或者借助工具库来实现。这样既能在编译时得到类型检查,也能在运行时保证对象不会被意外修改。
备注:内容来源于stack exchange,提问作者citykid

