在 TypeScript 中,我们如果想把某个类型中的所有属性都变为只读属性,在不使用泛型工具类型的情况下,我们可能会这么写:
typescript
interface Person {
readonly id: number;
readonly name: string;
readonly age: number;
readonly gender: string;
}
在上面这段代码中,我们定义了一个名为 Person 的接口,然后给里面的每个属性名前面加上 readonly 关键字,此时 Person 里面的所有属性都变为只读属性。
在我们声明对象,并且指定类型为 Person 时,我们就必须提供 Person 里面的所有属性。
typescript
const person: Person = {
id: 1,
name: 'Echo',
age: 26,
gender: 'Male',
}
以上的方法,可以将某个类型的所有属性变为只读属性,但写起来比较繁琐,代码的可维护性也比较差,而 TS 为我们提供了一个内置的泛型工具类型 Readonly<T>
。
1. 定义
在 TypeScript 中,泛型工具类型 Readonly<T>
主要用于将指定类型的所有属性设置为只读属性,也就是说,这些属性不能被重新赋值。
2. 源码
Readonly 在 TypeScript 中的源码实现:
typescript
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
实现原理:
type Readonly<T>
:使用关键字 type 定义一个类型别名 Readonly ,它接收泛型 T 作为参数。keyof T
:通过 keyof 操作符获取泛型 T 中的所有 key(可以理解成获取对象中的属性名),它返回的是由所有属性名组成的联合类型。in
:用于遍历泛型 T 中的每个属性名。T[P]
:获取泛型 T 中 P 的类型。(可以理解成 JS 中访问对象属性值的方式)。readonly
:将类型 T 的每个属性都设置为只读属性。{ readonly [P in keyof T]: T[P] }
:这是一个映射类型的语法,通过遍历 keyof T 返回的联合类型,然后使用变量 P 来接收,P 就相当于对象中的 key,然后在 key 前面加上 readonly,表示属性是只读属性,每一次遍历返回的值为 T[P]。
下面我们先看下 Readonly 的基本用法:
ini
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = Readonly<Person>;
上面这段代码中,定义了一个名为 Person 的接口,里面有2个必选属性 name 和 age。接着,我们使用泛型工具类型 Readonly 创建一个新的类型,将 Person 中的所有属性设置为只读的属性,那么 ReadonlyPerson 的类型将等同于下面的这段代码:
typescript
type ReadonlyPerson = {
readonly name: string;
readonly age: number;
}
3. 使用场景
3.1. 防止意外修改对象的属性
typescript
interface Person {
name: string;
age: number;
}
// 这里使用 Readonly<Person> 将接口 Person 中所有的属性都设置为只读属性
let readonlyPerson: Readonly<Person> = {
name: "Echo",
age: 26,
}
// 可以读取属性
console.log(readonlyPerson.name);
// 但是不能对属性重新赋值
readonlyPerson.name = "Steven"; // 报错:无法为"name"赋值,因为它是只读属性
上面这段代码中,我们定义了一个名为 Person 的接口,里面有2个必选属性 name 和 age。接着,定义了一个对象 readonlyPerson,类型为 Readonly,将 Person 中的所有属性设置为只读的属性,此时,我们可以读取对象中的属性,但是不能对属性重新赋值。
3.2. 作为函数参数类型
在函数参数中使用 Readonly 可以确保函数内部不会修改传入的对象的属性。
typescript
interface Person {
name: string;
age: number;
}
const getValueByKey: (person: Readonly<Person>) => void = person => {
console.log(person.name); // 正确:输出 Echo
console.log(person.age); // 正确:输出 26
// person.name = "James"; // 报错:无法为"name"赋值,因为它是只读属
// person.age = 36; // 报错:无法为"age"赋值,因为它是只读属性
}
let person: Person = {
name: "Echo",
age: 26,
}
getValueByKey(person)
对于数组,我们可以使用 ReadonlyArray 来创建一个只读的数组。
swift
// 使用 ReadonlyArray<number> 来创建一个 number 类型的只读的数组
let readonlyArr: ReadonlyArray<number> = [1, 2, 3];
// readonlyArr.push(4); // 报错:类型"readonly number[]"上不存在属性"push"
4. 注意事项
需要注意的是,如果我们使用 Readonly 将指定类型的所有属性设置为只读属性,不能再对对象中的属性重新赋值。
而且,Readonly<T>
只会将直接属性变为只读,而不会影响嵌套的属性。
typescript
interface Person {
id: number;
name: string;
age: number;
children: {
name: string;
gender: string;
school: string;
city: string;
}
}
type ReadonlyPerson = Readonly<Person>
上面这段代码中,ReadonlyPerson的类型等同于下面这段代码:
typescript
type ReadonlyPerson = {
readonly id: number;
readonly name: string;
readonly age: number;
readonly children: {
name: string;
gender: string;
school: string;
city: string;
};
}
还有另外一个需要注意的是:在 TypeScript 中,Readonly<T>
工具类型不仅可以作用于属性级别的只读性,而且也作用于包含在对象中的函数或方法。这意味着通过 Readonly<T>
创建的只读对象的属性和方法都不能被修改。
typescript
interface Person {
name: string;
age: number;
sayHello: () => void;
}
const person: Readonly<Person> = {
name: 'Echo',
age: 26,
sayHello: () => {
console.log(`Hello, my name is ${person.name}`);
},
};
// 可以调用属性和方法
console.log(person.name); // 输出:Echo
person.sayHello(); // 输出:Hello, my name is Echo
// 报错:不能为属性和方法重新赋值
person.name = 'Steven'; // 报错:无法为"name"赋值,因为它是只读属性。
person.sayHello = () => { // 报错:无法为"sayHello"赋值,因为它是只读属性。
console.log("Hello, world!");
};