「不変性」という名の防波堤:ReadonlyArrayとReadonlyTupleが救う、大規模フロントエンドの静かな崩壊
現場でコードレビューをしていると、未だに「とりあえず `Array
特に、Reactの `useState` や `useReducer`、あるいはReduxのような状態管理ライブラリにおいて、意図せず元の配列を破壊(mutation)してしまうバグは、UIのレンダリング不整合という「追跡困難な悪夢」を引き起こします。
今日は、TypeScriptの型システムを武器に、配列とタプルを「不変(Immutable)」という聖域に閉じ込めるための、実践的なアーキテクチャ設計について語りましょう。
—
1. なぜ Array ではなく ReadonlyArray なのか?
JavaScriptにおいて、配列は参照型です。`const` で宣言したとしても、`push` や `splice` を使えば中身は平気で書き換わります。これが「意図しない副作用」の温床です。
`ReadonlyArray
// 悪い例:可変な配列は、どこで誰に書き換えられるか監視できない
const users: string[] = [‘Alice’, ‘Bob’];
users.push(‘Charlie’); // 簡単に汚染される
// 良い例:ReadonlyArrayで「読み取り専用」であることを明示する
const users: ReadonlyArray
// users.push(‘Charlie’); // エラー: プロパティ ‘push’ は存在しません
// これにより、この配列がこのスコープ内で変更されないことが保証される
この「コンパイルエラーを出す」という行為こそが、我々エンジニアにとって最大の防波堤です。特に、非同期通信で取得したデータや、設定ファイルのように「初期化後は定数であるべきデータ」は、迷わず `ReadonlyArray` にすべきです。
—
2. ReadonlyTuple:構造を固定し、型安全を極限まで高める
タプル(Tuple)は配列の一種ですが、より厳格な「要素の順序」と「型」を定義します。Reactの `useState` の戻り値がまさにこれですね。
// [ユーザー名, 権限レベル] という構造を固定する
type UserTuple = readonly [string, number];
const admin: UserTuple = [‘Admin’, 0];
// admin[0] = ‘Guest’; // エラー: Index signature in type ‘readonly [string, number]’ only permits reading.
`readonly` を付与したタプルは、まさに鉄壁です。これは、単なるバリデーションではなく、メモリ効率の観点からも重要です。エンジン(V8など)が「この配列は今後一生変わらない」と静的解析で推測できれば、最適化の余地が生まれます。また、関数間でデータを渡す際に「このデータ構造は絶対に変更されない」という強い保証があることで、非同期処理の競合リスクを劇的に下げることができます。
—
3. パフォーマンスとレンダリングの最適化への影響
フロントエンドのパフォーマンスを語る上で欠かせないのが「参照の同一性(Referential Equality)」です。
Reactなどのライブラリは、メモ化(`useMemo` や `React.memo`)において「前回のpropsと今回のpropsが同じか?」を浅い比較(Shallow Compare)で行います。ここで、ミュータブルな配列を渡していると、中身が書き換わったとしても参照先が変わらないため、再レンダリングがトリガーされず、画面が更新されないという不可解なバグが発生します。
逆に、`ReadonlyArray` を活用し、変更のたびに新しい配列を生成(`[…oldArray, newItem]`)するイミュータブルな更新パターンを徹底すれば、参照比較による最適化が正しく機能します。
// パフォーマンスを意識した更新パターン
const [items, setItems] = useState
const addItem = (newItem: number) => {
// 元の配列を破壊せず、新しい配列を生成して参照を更新する
// これにより、Reactのメモ化が正しく機能する
setItems((prev) => […prev, newItem]);
};
—
4. 現場の知見: `as const` の魔法
TypeScriptにおいて、`Readonly` 型を一つずつ書くのは正直面倒です。そこで最強の武器が `as const` です。
// これだけで、配列の中身まで再帰的に readonly になる
const CONFIG = [
{ id: 1, label: ‘Home’ },
{ id: 2, label: ‘Settings’ }
] as const;
// CONFIG[0].id = 99; // エラー: 読み取り専用です
`as const` は単なる不変性付与ではありません。リテラル型を保持することで、型推論の精度を限界まで引き上げます。これを使って定数定義を管理するだけで、アプリケーションの堅牢性は一段階上のステージに引き上げられます。
最後に:エンジニアとしての矜持
`ReadonlyArray` や `ReadonlyTuple` を使うことは、単なる「型パズル」ではありません。それは、「このデータがいつ、どこで、どのように変化するか」という複雑性を最小化するための設計思想です。
あなたのコードが数年後、あるいは見知らぬ誰かによって保守されるとき、この「不変性」という静かな規律が、どれほど多くの不可解なバグを未然に防いでいるか。それに気づいたとき、あなたはまた一つ、本物のアーキテクトに近づいたと言えるはずです。
さあ、今すぐプロジェクトの型定義を見直し、`readonly` を追加する旅に出ましょう。その小さな一手間が、大規模なアプリケーションの未来を救います。

コメント