Menu

TypeScript readonly:只读属性、Readonly<T> 与只读数组

readonly 修饰符和 Readonly<T> 工具类型可以阻止代码重新给属性赋值。本文讲解只读属性和类的只读字段、Readonly<T>、只读数组(readonly T[] 和 ReadonlyArray)、ReadonlyMap 和 ReadonlySet,readonly 为什么是浅层的、只在编译时生效,以及它和 Object.freeze、as const 的比较。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

readonly 标记的属性只能在对象创建时设置一次,之后不能再重新赋值。Readonly<T> 把它应用到一个类型的每个属性上,readonly T[] 则对数组做同样的事:

最后一行展示了关于 readonly 最重要的事实:它由编译器检查,而不是在运行时强制执行。这次赋值是编译错误(这里用 @ts-expect-error 屏蔽了),但生成的 JavaScript 仍然执行了它。如果去掉屏蔽,文件就无法编译,而这正是 readonly 发挥作用的地方。

只读属性

在接口、类型字面量或类中,把 readonly 写在属性名前面。这个属性可以初始化,但不能重新赋值:

在类中,readonly 字段只能在声明处或构造函数中赋值,其他地方都不行。最简短的写法是参数属性 constructor(readonly id: string) {},它一步完成字段的声明和赋值。类页面 介绍了字段和构造函数的一般用法。

Readonly<T>:一次处理所有属性

Readonly<T> 是一个工具类型,把 T 的每个属性都标记为 readonly。对于你会到处传递但不应修改的值(例如应用状态),它很有用:

函数签名告诉读者,addItem 返回一个新状态,而不是修改旧状态,编译器也会让函数遵守这一点。Readonly<T> 的定义是映射类型 { readonly [P in keyof T]: T[P] }。

只读数组:readonly T[] 与 ReadonlyArray<T>

readonly number[] 和 ReadonlyArray<number> 是同一个类型。它们去掉了所有会修改数组的方法(push、pop、shift、splice、sort、reverse、fill 等),并禁止按索引赋值。不修改数组的方法仍然保留,并返回普通数组:

把参数类型写成 readonly T[],就是向调用方承诺你不会修改他们的数组。反过来则是大家容易卡住的地方:只读数组不能传给接受普通 T[] 的函数,因为那个函数可能会修改它。

index.ts(7,17): error TS4104: The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.

修复方法是把 sum 改为接受 readonly number[],因为它并不修改任何东西。只读取数组的函数都应该接受只读类型,这样两种数组它都能接受。如果函数不归你管,就传一个副本:sum([...prices])。

ReadonlyMap 与 ReadonlySet

Map 和 Set 也有只读版本。ReadonlyMap<K, V> 有 get、has、size、forEach 和各种迭代器,但没有 set、delete 或 clear;ReadonlySet<T> 没有 add、delete 或 clear:

类常常在内部保存一个私有的可变 Map,再通过类型为 ReadonlyMap 的 getter 对外暴露,这样外部代码可以读取数据,却不能通过这个引用修改它。

readonly 是浅层的

readonly 和 Readonly<T> 只保护属性本身,而不保护它指向的对象或数组:

DeepReadonly<T> 会把自己应用到每一个嵌套的对象类型上;而对数组类型做映射会得到只读数组,所以 members 变成了 readonly string[]。这仍然只是类型层面的承诺,而不是运行时的保护。

只在编译时生效:通过另一个引用修改

只读类型控制的是某一个引用能做什么。指向同一对象、但类型中没有 readonly 的另一个引用可以修改它,而且 TypeScript 甚至允许把只读类型赋给可变类型:

mutable = settings 这次赋值能编译通过,因为 TypeScript 在检查两个对象类型是否兼容时,不考虑 readonly 属性;TypeScript 手册明确说明了这一点,并指出只读属性因此可能通过别名被修改。只读数组则不同:上面的 TS4104 错误正是这项检查。Object.freeze 能真正在运行时阻止修改:生成的代码在严格模式下运行,而在严格模式下写入被冻结的属性会抛出 TypeError。和 readonly 一样,Object.freeze 也是浅层的。

readonly、const、as const 与 Object.freeze

对字面量使用 as const,会让每一层的每个属性都变成 readonly,并保留字面量类型,这通常是得到深层只读值最简单的方法:

const theme = { mode: "dark", sizes: [12, 14] } as const;
// { readonly mode: "dark"; readonly sizes: readonly [12, 14] }
作用于深层?运行时效果示例
const变量绑定否变量不能被重新赋值const user = {...}
readonly一个属性或数组类型否无readonly id: string
Readonly<T>一个类型的每个属性否无Readonly<State>
as const字面量表达式是无{ ... } as const
Object.freeze对象值否写入失败(严格模式下抛出异常)Object.freeze(obj)

const 和 readonly 回答的是不同的问题:const 阻止名字指向别处,readonly 阻止属性被修改。const 对象的属性仍然可以重新赋值,除非它们是 readonly。

常见问题

TypeScript 中 readonly 有什么作用?

readonly 标记的属性可以在对象创建时(或在类的构造函数中)设置,但之后不能重新赋值。之后再给它赋值会导致编译错误 TS2540。它只是类型检查:生成的 JavaScript 中没有任何保护。

TypeScript 中 readonly 和 const 有什么区别?

const 针对的是变量:这个名字不能再指向另一个值,但它持有的对象仍然可以修改。readonly 针对的是属性:这个属性不能被重新赋值。const user = { name: "Ada" } 仍然允许 user.name = "x";而 readonly name 属性不允许。

TypeScript 中如何让数组只读?

把它标注为 readonly T[] 或 ReadonlyArray<T>(两者是同一个类型)。push、pop、sort、splice 这类会修改数组的方法会从类型中消失,按索引赋值也会报错。map、filter、slice 这类不修改数组的方法仍然可用,并返回普通数组。

TypeScript 的 Readonly 是深层的吗?

不是。Readonly<T> 和 readonly 只保护顶层属性;内部嵌套的对象和数组仍然可以修改。要在类型层面实现深层保护,对字面量使用 as const,或者编写一个递归的 DeepReadonly<T> 类型。

readonly 能在运行时阻止修改吗?

不能。类型会被擦除,所以 readonly 属性在运行时就是普通属性,持有同一对象可变引用的代码(或普通 JavaScript)仍然可以修改它。需要运行时保护时使用 Object.freeze;TypeScript 会把它的结果类型标注为 Readonly<T>。

Coddy 编程语言插图

和 Coddy 一起学编程

开始