关于 use-readonly
面向 TypeScript 开发者的技能,讲解如何用 readonly 标注输入和属性,帮助在编译期发现意外修改。还说明其浅层限制及运行时行为。
主要功能
- 01用 readonly 标注数组参数
- 02用 Readonly 标注对象
- 03为不应更改的属性添加只读约束
- 04区分浅层只读与运行时不可变
- 05通过复制后修改来避免输入变更
适用场景
- 01设计不会修改输入的函数签名
- 02排查数组或对象意外变化
- 03处理共享状态与 API 输入
- 04评估性能后决定是否采用变更
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill use-readonly安装后在 Claude Code 中输入/use-readonly确认已加载。
详细介绍
使用 readonly 避免与修改相关的错误
概述
readonly 表明了设计意图,也能让 TypeScript 捕获意外修改。
修改是 bug 的常见来源。readonly 可以帮助 TypeScript 在编译时捕获这类问题,也能向其他开发者说明你的意图。
何时使用此技巧
- 将数组或对象传递给函数时
- 调试意外的数据变化时
- 设计函数签名时
- 使用共享状态时
- 创建不应修改输入的 API 时
铁律
NEVER mutate function parameters unless mutation is the explicit purpose.
没有例外:
- 不能因为“这样效率更高”
- 不能因为“没有其他人使用这些数据”
- 不能因为“我会记得不去修改”
检测:“修改异味”
如果函数接收数组或对象并对其进行修改,请考虑这是否符合预期。
// ❌ VIOLATION: Function mutates its input
function printTriangles(n: number[]) {
n.sort((a, b) => a - b); // Mutates the original array!
console.log(n);
}
const nums = [5, 3, 8, 1];
printTriangles(nums);
console.log(nums); // [1, 3, 5, 8] - Original changed!
// ✅ CORRECT: Use readonly to prevent mutation
function printTriangles(n: readonly number[]) {
n.sort((a, b) => a - b);
// ~~~~ Property 'sort' does not exist on 'readonly number[]'
}
数组的 readonly
// readonly number[] - can read, cannot modify
function sum(arr: readonly number[]): number {
arr.push(1); // Error: Property 'push' does not exist
arr[0] = 5; // Error: Index signature only permits reading
return arr.reduce((a, b) => a + b, 0); // OK: reading is fine
}
// If you need to modify, copy first:
function sortedCopy(arr: readonly number[]): number[] {
return [...arr].sort((a, b) => a - b); // Copy, then sort
// Or use toSorted() which doesn't mutate:
return arr.toSorted((a, b) => a - b);
}
对象的 readonly
interface Point {
x: number;
y: number;
}
// Readonly<T> makes all properties readonly
function movePoint(p: Readonly<Point>, dx: number, dy: number): Point {
p.x += dx; // Error: Cannot assign to 'x' because it is a read-only property
// Return a new object instead
return { x: p.x + dx, y: p.y + dy };
}
readonly 属性
interface Config {
readonly apiUrl: string;
readonly timeout: number;
}
const config: Config = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
config.apiUrl = 'https://other.com'; // Error: Cannot assign to read-only property
readonly 是浅层的
interface Outer {
readonly inner: { value: number };
}
const obj: Outer = { inner: { value: 1 } };
obj.inner = { value: 2 }; // Error: readonly
obj.inner.value = 2; // OK! Nested object is not readonly
// For deep readonly, use DeepReadonly utility or as const
const deepObj = {
inner: { value: 1 }
} as const;
// ^? const deepObj: { readonly inner: { readonly value: 1 } }
抵御压力的准则
1. “这样效率更高”
压力:“复制数组很慢,修改更快”
**回应:**正确性 > 微优化。如果性能很重要,请先测量。
**行动:**默认使用 readonly。只有性能分析证明确有需要时才进行修改。
2. “所有调用方都由我控制”
压力:“没有其他人会调用这个函数”
**回应:**代码会不断演变。6 个月后你不会记得这件事。
**行动:**面向未来进行设计。readonly 是一份长久有效的文档。
3. “readonly 太冗长了”
压力:“到处添加 readonly 很麻烦”
**回应:**这只是一次性的成本。它能避免 bug,为你节省时间。
**行动:**先从函数参数开始,再逐步扩展。
readonly 与不可变
// readonly is a compile-time check, not a runtime guarantee
const arr: readonly number[] = [1, 2, 3];
// TypeScript prevents this:
arr.push(4); // Error
// But at runtime, it's still a regular array:
(arr as number[]).push(4); // Works at runtime!
// For true immutability, use Object.freeze():
const frozen = Object.freeze([1, 2, 3]);
// ^? const frozen: readonly number[]
危险信号——立即停止并重新考虑
- 在参数上调用
.push()、.sort()或.splice()的函数 - 类似“// 不要修改此数组”的注释
- 数据意外变化的 bug
- 被多个函数修改的共享状态
- 在函数中重新赋值的参数
常见的自我辩解(都不成立)
| 借口 | 事实 |
|---|---|
| “这只是一个内部函数” | 内部代码同样需要正确性。 |
| “复制的代价很高” | 先通过基准测试证明这一点。 |
| “我会写文档说明” | 类型比注释更适合作为文档。 |
| “TypeScript 太严格了” | TypeScript 正在保护你免受修改导致的 bug。 |
快速参考
| 你有 | 使用 | 效果 |
|---|---|---|
number[] 参数 | readonly number[] | 防止修改 |
| 对象参数 | Readonly<T> | 浅层只读 |
| 深层嵌套 | as const | 深层只读 |
| 数组返回值 | readonly T[] | 表明不可变性 |
不可变模式
// Instead of mutating, return new values:
// ❌ Mutating
function addItem(arr: string[], item: string) {
arr.push(item);
}
// ✅ Immutable
function addItem(arr: readonly string[], item: string): string[] {
return [...arr, item];
}
// ❌ Mutating object
function updateUser(user: User, name: string) {
user.name = name;
}
// ✅ Immutable
function updateUser(user: Readonly<User>, name: string): User {
return { ...user, name };
}
总结
readonly 是由 TypeScript 强制执行的契约。
在函数参数上使用它,以防止意外修改。在不应更改的属性上使用它。让 TypeScript 在编译时捕获修改导致的 bug,而不是等到运行时再调试。
参考
内容基于 Dan Vanderkam 所著《Effective TypeScript》第 14 条:使用 readonly 避免与修改相关的错误。
评论
支持 Markdown