【実務・中級編】 ReadonlyArrayとReadonlyTuple – TypeScript実践ガイド

エンジニアの皆さん、お疲れ様です。フロントエンドの現場で「なぜか値が書き換わっている」「どこで誰がこの配列を汚したのかわからない」という、いわゆる“ミューテーションの悪夢”に悩まされた経験はありませんか?

Reactの`useState`や`useReducer`を扱っていると、イミュータブル(不変)なデータ設計の大切さは痛感するはずです。今日は、TypeScriptでそのイミュータブルな設計を強制するための最強の武器、『`ReadonlyArray`と`ReadonlyTuple`』について、現場の視点から深掘りしていきましょう。

—

なぜ、ただの `Array` では不安なのか

TypeScriptの標準的な `number[]` や `Array` は、実はかなり「だらしない」型です。なぜなら、これらには `push` や `splice` といった「自分自身を破壊するメソッド」が許容されているからです。

const numbers: number[] = [1, 2, 3];
numbers.push(4); // 何の警告も出ない。これ、意図せぬ副作用の温床です。

中規模以上のアプリケーションで、関数に配列を渡した際、その関数内で破壊的変更が行われていたら……デバッグは地獄絵図になります。ここで登場するのが `ReadonlyArray` です。

—

現場で使うべき `ReadonlyArray` とその構文

`ReadonlyArray` を使うと、TypeScriptはコンパイル時に「その配列をいじろうとする行為」を全て弾いてくれます。

1. ReadonlyArray の基本

型定義としては `ReadonlyArray` を使うか、省略記法の `readonly T[]` を使います。実務では可読性の観点から後者が好まれます。

// 推奨:readonly 修飾子を使う
const items: readonly string[] = [“React”, “TypeScript”, “Next.js”];

// items.push(“Vue”); // Error: Property ‘push’ does not exist on type ‘readonly string[]’.
// items[0] = “Angular”; // Error: Index signature in type ‘readonly string[]’ only permits reading.

// 読み取り専用なので、mapやfilterは使えます(これらは新しい配列を返すため)
const upperItems = items.map(item => item.toUpperCase());

2. ReadonlyTuple:固定長の不変リスト

タプル(Tuple)は「型が固定された配列」ですが、これも `readonly` をつけることで真価を発揮します。APIのレスポンスや、関数の戻り値で「この値は絶対に書き換えてはいけない」と明示したい時に重宝します。

// 座標データのような、絶対に書き換わってはいけない固定長データ
const point: readonly [number, number] = [10, 20];

// point[0] = 50; // Error: 読み取り専用です

—

裏側の挙動:ブラウザは「不変」をどう解釈しているのか

ここで一つ、シニアとして教えたい視点があります。`readonly` はあくまで「TypeScriptのコンパイル時のみ」有効なガードレールだということです。

JavaScriptのランタイム(ブラウザのエンジン)には `readonly` という概念は存在しません。コンパイルされた後のJSコードでは、普通の配列として処理されます。

「じゃあ、JSからいじれば書き換えられるじゃないか」と思うかもしれませんが、それは正しい。だからこそ、TypeScriptの役割は「開発中のヒューマンエラーを仕組みで殺す」ことに特化しているのです。`Object.freeze()` と組み合わせることで、ランタイム側までガチガチに固めることも可能ですが、パフォーマンスとのトレードオフになるため、基本は「型による契約」で守るのがフロントエンドの作法です。

—

実務で役立つ「as const」の魔法

中級者レベルなら必ずマスターしておきたいのが `as const` です。これを使うと、オブジェクトや配列を「再帰的に読み取り専用」として推論させることができます。

// 状態管理の定数などで非常によく使うパターン
const STATUS = {
PENDING: “pending”,
SUCCESS: “success”,
ERROR: “error”,
} as const;

// STATUS.PENDING = “done”; // Error: 読み取り専用です!
// これにより、定数管理が劇的に安全になります。

—

まとめ:明日からチームでやるべきこと

1. 関数の引数には `readonly` をつける: 関数内で配列をいじる必要がないなら、必ず `readonly T[]` を受け取るように設計してください。これだけでバグの発生率が数割減ります。
2. 定数は `as const` で締める: マジックナンバーや設定値は、とりあえず `as const` をつける癖をつけましょう。
3. 破壊的メソッドを使わない: `push` や `pop` の代わりに `[…array, newItem]` のようにスプレッド構文で新しい配列を作る設計に慣れてください。

型定義は、単なるコードの装飾ではありません。「将来の自分やチームメンバーへのメンテナンス性の約束」です。`readonly` を使いこなし、堅牢で美しいコードベースを一緒に築いていきましょう。

何か分からないことや、現場特有の複雑なケースがあれば、いつでも相談してください。コードは裏切りませんが、設計の甘さは必ず裏切りますよ。頑張りましょう!

コメント

タイトルとURLをコピーしました