【テクニカル・上級編】 ReadonlyArrayとReadonlyTuple – TypeScript実践ガイド

「不変性」という名の防波堤:ReadonlyArrayとReadonlyTupleが救う、大規模フロントエンドの静かな崩壊

現場でコードレビューをしていると、未だに「とりあえず `Array` を使っておけばいい」という風潮に出会うことがあります。しかし、大規模で複雑な状態管理が求められるWebアプリケーションにおいて、ミュータブル(可変)な配列を安易に許容することは、時限爆弾を抱えて走るようなものです。

特に、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 = [‘Alice’, ‘Bob’];

// 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>([1, 2, 3]);

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` を追加する旅に出ましょう。その小さな一手間が、大規模なアプリケーションの未来を救います。

コメント

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