TypeScript readonly 详解:编译期不可变性
readonly 在编译期防止属性与数组被修改。本文讲解 readonly 属性、ReadonlyArray、与 const/Object.freeze 的三角关系。
readonly 是编译期约束:readonly name: string 的属性不能重新赋值,readonly string[] 的数组不能 push。与 const(防变量重绑)和 Object.freeze(运行时浅冻结)互补——三者各管一层。
可变性问题
function processUsers(users: { name: string }[]) {
users[0].name = '篡改'; // 调用方的数据被悄悄改了
}
函数修改传入对象是隐蔽 bug 的温床——readonly 把"不许改"变成编译期契约。
readonly 属性
interface Config {
readonly apiUrl: string;
readonly timeout: number;
}
const config: Config = { apiUrl: 'https://api.example.com', timeout: 5000 };
config.timeout = 3000; // 编译错误
Readonly<T> 工具类型一键全只读:Readonly<User>。
只读数组与元组
const list: readonly string[] = ['a', 'b'];
list.push('c'); // 编译错误
list[0] = 'x'; // 编译错误
// ReadonlyArray<string> 等价
function sum(nums: readonly number[]) { /* 保证不改传入数组 */ }
函数签名用 readonly 数组是善意声明:我不动你的数据——调用方传数据时无后顾之忧。
readonly vs const vs Object.freeze
| const | readonly | Object.freeze | |
|---|---|---|---|
| 管什么 | 变量重绑 | 属性/元素修改 | 运行时浅冻结 |
| 时机 | 编译期 | 编译期 | 运行时 |
const obj = { a: 1 } as const; // as const = readonly + 字面量化
obj.a = 2; // 编译错误
as const 是 readonly 的激进形态:全属性只读 + 字面量类型。
边界
readonly 是编译期约束——运行时数据照样能被 JS 改。跨边界(序列化进出)后约束消失。需要运行时保障的场景配合 Object.freeze(注意:浅冻结,嵌套对象要深冻结库)。
常见问题(FAQ)
Q:readonly 数组能转回可变数组吗?
需要显式拷贝:[...readonlyArr]。类型系统故意制造摩擦——转换点即审计点。
Q:React state 该用 readonly 吗?
推荐——state 不可变是 React 的心智模型,Readonly props 类型让"不许直接改 props"从约定变为编译强制。
Q:深只读怎么做?
手写递归类型(DeepReadonly,见本系列《映射类型》)或用 ts-essentials 库的现成实现。
参考文档
本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。