mcpField
U
use-readonly

use-readonly

github.com/marius-townhouse开发648 次安装39 周前更新
代码仓库

关于 use-readonly

面向 TypeScript 开发者的技能,讲解如何用 readonly 标注输入和属性,帮助在编译期发现意外修改。还说明其浅层限制及运行时行为。

主要功能

  1. 01用 readonly 标注数组参数
  2. 02用 Readonly 标注对象
  3. 03为不应更改的属性添加只读约束
  4. 04区分浅层只读与运行时不可变
  5. 05通过复制后修改来避免输入变更

适用场景

  1. 01设计不会修改输入的函数签名
  2. 02排查数组或对象意外变化
  3. 03处理共享状态与 API 输入
  4. 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