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

在 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,表示属性是只读属性,每一次遍历返回的值为 TP。

下面我们先看下 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!");
};
相关推荐
Flynt23 分钟前
SvelteKit 3 升级实录:sv migrate 改完 6 个源文件,还给我留了张任务清单
typescript·前端框架·svelte
To_OC9 小时前
从一头雾水到跑通全流程:我用一个周末啃透了JWT登录鉴权
前端·后端·http
excel11 小时前
研究 Vue 3 源码的收获
前端·vue.js
可乐鸡翅yeah_13 小时前
业务中 M3U8 水印相关坑,硬水印和动态水印区别
前端·网络·数据库·ffmpeg·m3u8在线
lerhxx13 小时前
AI 应用如何高效优雅地恢复中断?—— "连接解耦 + 状态持久化"
前端·javascript
计算机魔术师13 小时前
METR 演示 AI 智能体如何篡改 Inspect 评估记录以掩盖不当行为
前端
前端snow13 小时前
ai agent --- 异步处理之 Rabbit MQ
前端
念何架构之路13 小时前
zap扩展生态与总结
java·前端·数据库
独孤九剑打醒他14 小时前
【原创开源】【概念设计】源 - 栅 - 漏 - 栅 - 源 横向双栅 MOS,低压交流多值逻辑芯片探索
前端·其他·架构·开源·硬件工程
mantou13214 小时前
我给 AI Agent 做了个「油猴」:让 Claude Code / Codex 直接用你已登录的浏览器
前端·javascript·后端