likes
comments
collection
share

一文读懂TypeScript泛型工具类型Readonly<T>

作者站长头像
站长
· 阅读数 13

在 TypeScript 中,我们如果想把某个类型中的所有属性都变为只读属性,在不使用泛型工具类型的情况下,我们可能会这么写:

interface Person {
  readonly id: number;
  readonly name: string;
  readonly age: number;
  readonly gender: string;
}

在上面这段代码中,我们定义了一个名为 Person 的接口,然后给里面的每个属性名前面加上 readonly 关键字,此时 Person 里面的所有属性都变为只读属性。

在我们声明对象,并且指定类型为 Person 时,我们就必须提供 Person 里面的所有属性。

const person: Person = {
  id: 1,
  name: 'Echo',
  age: 26,
  gender: 'Male',
}

以上的方法,可以将某个类型的所有属性变为只读属性,但写起来比较繁琐,代码的可维护性也比较差,而 TS 为我们提供了一个内置的泛型工具类型 Readonly<T>

1. 定义

在 TypeScript 中,泛型工具类型 Readonly<T> 主要用于将指定类型的所有属性设置为只读属性,也就是说,这些属性不能被重新赋值。

2. 源码

Readonly 在 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 的基本用法:

interface Person {
  name: string;
  age: number;
}

type ReadonlyPerson = Readonly<Person>;

上面这段代码中,定义了一个名为 Person 的接口,里面有2个必选属性 name 和 age。接着,我们使用泛型工具类型 Readonly 创建一个新的类型,将 Person 中的所有属性设置为只读的属性,那么 ReadonlyPerson 的类型将等同于下面的这段代码:

type ReadonlyPerson = {
  readonly name: string;
  readonly age: number;
}

3. 使用场景

3.1. 防止意外修改对象的属性

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 可以确保函数内部不会修改传入的对象的属性。

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 来创建一个只读的数组。

// 使用 ReadonlyArray<number> 来创建一个 number 类型的只读的数组
let readonlyArr: ReadonlyArray<number> = [1, 2, 3];

// readonlyArr.push(4); // 报错:类型“readonly number[]”上不存在属性“push”

4. 注意事项

需要注意的是,如果我们使用 Readonly 将指定类型的所有属性设置为只读属性,不能再对对象中的属性重新赋值。

而且,Readonly<T> 只会将直接属性变为只读,而不会影响嵌套的属性。

interface Person {
  id: number;
  name: string;
  age: number;
  children: {
    name: string;
    gender: string;
    school: string;
    city: string;
  }
}

type ReadonlyPerson = Readonly<Person>

上面这段代码中,ReadonlyPerson的类型等同于下面这段代码:

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> 创建的只读对象的属性和方法都不能被修改。

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!");
};