TypeScript 不可变性 readonly, Readonly<T>的使用小结
作者:哈希茶馆
哈喽,大家好,欢迎来到【哈希茶馆】!在日常的全栈开发中,我们经常会遇到因数据被意外修改而导致的各种诡异 Bug。特别是在 JavaScript 这种动态语言中,对象的属性默认是可变的,这在大型或多人协作的项目中很容易引入不确定性。TypeScript 作为 JavaScript 的超集,为我们提供了强大的类型系统,其中就包括了实现不可变性的有效工具。今天,我们就来聊聊如何使用 readonly 和 Readonly<T> 来提升代码的健壮性和可维护性。
什么是不可变性?为什么它很重要?
简单来说,不可变性(Immutability)指的是一个对象在创建之后,其状态就不能再被修改。如果我们想改变这个对象,不能直接修改原始对象,而是应该创建一个新的对象,这个新对象拥有修改后的状态。
拥抱不可变性有诸多好处:
- 可预测性更高:当你知道一个数据结构不会被意外改变时,代码的行为就更容易预测和推理。
- 简化调试:如果应用状态是不可变的,追踪状态变更就变得简单,因为每次变更都会产生新的对象。
- 性能优化:在某些场景下(例如 React 的 PureComponent 或 Redux),通过比较对象的引用就能快速判断数据是否发生变化,从而避免不必要的渲染或计算。
- 更易于并发处理:不可变数据天然线程安全,因为它们不会被多个线程同时修改(虽然在前端 JavaScript 中并发场景相对较少,但这个理念依然重要)。
readonly修饰符:让属性只读
TypeScript 提供了 readonly 修饰符,可以用来标记类的属性或接口的属性,表示这些属性在对象初始化之后就不能再被修改。
1. 类属性的readonly
class UserProfile {
readonly userId: string;
name: string;
constructor(userId: string, name: string) {
this.userId = userId; // 可以在构造函数中赋值
this.name = name;
}
updateName(newName: string) {
this.name = newName; // 普通属性可以修改
}
// 尝试修改 readonly 属性会导致编译错误
// updateUserId(newUserId: string) {
// this.userId = newUserId; // Error: Cannot assign to 'userId' because it is a read-only property.
// }
}
const user = new UserProfile("user-123", "Alice");
console.log(user.userId); // "user-123"
user.updateName("Bob");
console.log(user.name); // "Bob"
// user.userId = "user-456"; // Error: Cannot assign to 'userId' because it is a read-only property.
在上面的例子中,userId 属性被 readonly 修饰后,只能在构造函数 constructor 中被初始化。之后任何尝试修改 userId 的行为都会在编译阶段被 TypeScript 编译器捕获并报错。
2. 接口属性的readonly
readonly 同样可以用于接口定义:
interface Point {
readonly x: number;
readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
// p1.x = 5; // Error: Cannot assign to 'x' because it is a read-only property.
function movePoint(p: Point, dx: number, dy: number): Point {
// 不能直接修改 p.x 和 p.y
// 要返回一个新的 Point 对象
return { x: p.x + dx, y: p.y + dy };
}
const p2 = movePoint(p1, 5, 5);
console.log(p1); // { x: 10, y: 20 } (原始对象未改变)
console.log(p2); // { x: 15, y: 25 } (返回了新对象)
这里,Point 接口的 x 和 y 属性都是只读的。当我们尝试修改 p1.x 时,编译器会报错。movePoint 函数也遵循了不可变性的原则,它没有修改传入的 Point 对象,而是返回了一个全新的 Point 对象。
注意:readonly 提供的是浅不可变性。如果 readonly 属性本身是一个对象,那么这个对象的属性仍然是可以修改的。
interface Config {
readonly serverUrl: string;
readonly settings: {
timeout: number;
retries: number;
};
}
const appConfig: Config = {
serverUrl: "https://api.example.com",
settings: {
timeout: 5000,
retries: 3,
},
};
// appConfig.serverUrl = "https://new.api.example.com"; // Error: Cannot assign to 'serverUrl' because it is a read-only property.
appConfig.settings.timeout = 10000; // 这是允许的!
console.log(appConfig.settings.timeout); // 10000
appConfig.settings 本身是 readonly 的,意味着你不能给 appConfig.settings 赋一个新的对象。但是 settings 对象内部的 timeout 属性依然是可变的。
Readonly<T>工具类型:创建全只读类型
当我们需要将一个现有类型的所有属性都变成只读时,TypeScript 的 Readonly<T> 工具类型就派上用场了。它会构造一个新类型,将类型 T 的所有属性都设置为 readonly。
interface MutableConfigs {
port: number;
debugMode: boolean;
features: string[];
}
// 使用 Readonly<T> 将 MutableConfigs 的所有属性变为只读
type ImmutableConfigs = Readonly<MutableConfigs>;
const initialConfigs: MutableConfigs = {
port: 3000,
debugMode: true,
features: ["featureA", "featureB"]
};
// immutableAppConfig 的类型是 ImmutableConfigs
const immutableAppConfig: ImmutableConfigs = initialConfigs;
// immutableAppConfig.port = 8080; // Error: Cannot assign to 'port' because it is a read-only property.
// immutableAppConfig.debugMode = false; // Error: Cannot assign to 'debugMode' because it is a read-only property.
// 同样,Readonly<T> 也是浅不可变的
// immutableAppConfig.features.push("featureC"); // 这仍然是允许的,因为 features 是一个数组对象
// console.log(immutableAppConfig.features); // ["featureA", "featureB", "featureC"]
// 如果要使数组也只读,可以这样定义:
interface TrulyImmutableConfigs {
readonly port: number;
readonly debugMode: boolean;
readonly features: ReadonlyArray<string>; // 使用 ReadonlyArray<T>
}
const trulyImmutableAppConfig: TrulyImmutableConfigs = {
port: 8000,
debugMode: false,
features: ["logging", "monitoring"]
};
// trulyImmutableAppConfig.features.push("newFeature"); // Error: Property 'push' does not exist on type 'readonly string[]'.
// trulyImmutableAppConfig.features[0] = "newLogging"; // Error: Index signature in type 'readonly string[]' only permits reading.
Readonly<T> 非常适合用于函数参数或返回值,以明确表示函数不会(或不应该)修改传入的对象,或者返回一个不应被修改的对象。
function processConfig(config: Readonly<MutableConfigs>): void {
console.log(`Processing config for port: ${config.port}`);
// config.port = 8081; // Error: Cannot assign to 'port' because it is a read-only property.
}
processConfig(initialConfigs);
ReadonlyArray<T>
对于数组,TypeScript 提供了 ReadonlyArray<T> 类型。一个 ReadonlyArray<T> 移除了所有会修改数组的方法(如 push, pop, splice 等),并且其索引签名也是只读的,防止通过索引直接修改元素。
let readOnlyNumbers: ReadonlyArray<number> = [1, 2, 3, 4, 5]; // readOnlyNumbers.push(6); // Error: Property 'push' does not exist on type 'readonly number[]'. // readOnlyNumbers[0] = 0; // Error: Index signature in type 'readonly number[]' only permits reading. // 可以使用不会修改原数组的方法 const doubledNumbers = readOnlyNumbers.map(n => n * 2); console.log(doubledNumbers); // [2, 4, 6, 8, 10] console.log(readOnlyNumbers); // [1, 2, 3, 4, 5] (原始数组未改变)
实践中的考量与深不可变性
readonly 和 Readonly<T> 为我们提供了在 TypeScript 中实现不可变性的基础工具。它们主要提供的是浅不可变性。这意味着如果一个只读属性本身是一个复杂对象(如对象或数组),那么这个复杂对象的内部属性仍然是可变的。
要实现深不可变性(Deep Immutability),即对象及其所有嵌套的子对象都不可变,通常需要:
- 递归地定义只读类型:手动为嵌套结构定义
readonly。 - 使用第三方库:像 Immer 或 Immutable.js 这样的库提供了更完善的深不可变数据结构和操作方法。
在大多数日常开发场景中,TypeScript 内建的浅不可变性已经能解决很多问题,特别是在定义配置对象、函数契约等方面。当处理复杂的状态管理(如 Redux store)时,可以考虑引入专门的不可变数据库。
总结
拥抱不可变性是提升代码质量、减少 Bug、增强系统可维护性的重要手段。TypeScript 的 readonly 修饰符和 Readonly<T> 工具类型为我们提供了在编译时强制实施不可变性的能力。
- 使用
readonly标记那些初始化后不应再改变的类属性或接口属性。 - 使用
Readonly<T>将现有类型的所有属性转换为只读,常用于函数参数和返回值。 - 对于数组,使用
ReadonlyArray<T>来确保数组内容不被修改。
虽然它们提供的是浅不可变性,但这已经是向前迈出的一大步。养成使用这些工具的习惯,你的代码会变得更加稳健和可预测。
到此这篇关于TypeScript 不可变性 readonly, Readonly<T>的使用小结的文章就介绍到这了,更多相关TypeScript 不可变性 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
