そのコード、破壊者(Destroyer)になっていないか?:ReadonlyTupleで築く不変の防壁
モダンなフロントエンド開発において、バグの温床となるのは常に「予期せぬ状態の変化(Side Effect)」だ。
特に、JavaScriptの配列メソッドには `sort()` や `reverse()`、`splice()` といった、元の配列を直接書き換えてしまう「破壊的メソッド」が平然と居座っている。
君が書いた関数が、引数で受け取った配列を不用意にソートしたとする。その瞬間、呼び出し元の React の State やキャッシュ層のデータは音もなく破壊され、原因不明の再レンダリング地獄や不整合なUIへと繋がる。
「気をつけて書く」という精神論は、大規模開発では無力だ。我々には TypeScriptの型システムによる強制力 が必要だ。今回は、関数の引数において `ReadonlyArray` とタプル型を組み合わせた 「ReadonlyTuple」 を活用し、コンパイルレベルで破壊的操作を封殺する極限の知見を伝授する。
—
1. なぜ `ReadonlyArray` だけでは不十分なのか
まず、基本を整理しよう。通常の `T[]` や `Array
// 悪い例:引数が可変であるため、関数内部で破壊的操作が許されてしまう
function processPositions(coords: number[]) {
// コンパイルエラーにならないが、呼び出し元の配列を破壊する
coords.sort();
return coords[0];
}
これに対し、`readonly number[]`(または `ReadonlyArray
実務で必要なのは、単なる「読み取り専用のリスト」ではなく、「構造が定義された読み取り専用のタプル」だ。
—
2. ReadonlyTuple:構造と不変性の融合
タプル型に `readonly` 修飾子を付与することで、我々は「特定の順序で、特定の型が並び、かつ一切の変更を許さない」という最強の制約を課すことができる。
プロダクション級のコード例:座標変換エンジンの設計
以下の例は、地理情報システムやグラフィック制御で使われる座標データを扱う関数だ。
/
- 緯度・経度・高度を保持する厳格な座標型
- 読み取り専用のタプルとして定義することで、インデックスごとの意味を固定する
/
type Coordinate = readonly [latitude: number, longitude: number, altitude: number];
/
- 複数の座標を受け取り、中心点を計算する関数
- ReadonlyTupleの配列を受け取ることで、入力データの順序変更や書き換えを完全に封殺する
/
function calculateCenterPoint(
// 読み取り専用配列の中に、読み取り専用タプルを格納
points: readonly Coordinate[]
): Coordinate {
// points.push(…) // Error: Property ‘push’ does not exist on type ‘readonly Coordinate[]’
// points[0][0] = 90; // Error: Index signature in type ‘Coordinate’ only permits reading
let sumLat = 0, sumLng = 0, sumAlt = 0;
for (const [lat, lng, alt] of points) {
sumLat += lat;
sumLng += lng;
sumAlt += alt;
}
const len = points.length;
// 戻り値もReadonlyTupleとして返し、後続の処理での破壊を許さない
return [sumLat / len, sumLng / len, sumAlt / len] as const;
}
// 使用例
const locationA: Coordinate = [35.6895, 139.6917, 0];
const locationB: Coordinate = [34.6937, 135.5023, 10];
const center = calculateCenterPoint([locationA, locationB]);
—
3. 型の共変性(Covariance)と「柔軟な堅牢性」
ここで、シニアエンジニアなら知っておくべき「型システムの深淵」に触れる。
なぜ、関数の引数には積極的に `readonly` をつけるべきなのか? それは 「Readonly型は、Mutable型よりも広い許容度を持つ」 からだ。
TypeScriptにおいて、`readonly T[]` は `T[]` のスーパータイプ(より広い型)である。
1. Mutableな配列 は、`ReadonlyArray` を期待している場所に渡すことができる。
2. しかし、Readonlyな配列 は、Mutableな配列を期待している場所に渡すことはできない。
function printItems(items: readonly string[]) { / … / }
function modifyItems(items: string[]) { / … / }
const myData: string[] = [“A”, “B”];
const frozenData: readonly string[] = [“C”, “D”];
printItems(myData); // OK: MutableはReadonlyとして扱える
printItems(frozenData); // OK
modifyItems(myData); // OK
modifyItems(frozenData); // Error: ReadonlyをMutableには渡せない(安全性の欠如)
つまり、関数の引数を `readonly` に設計するということは、「呼び出し側に不必要な制約(Mutableであれという制約)を課さない」 という、インターフェース設計における最高級の配慮なのだ。
—
4. 実務応用:Variadic Tuple Typesとの組み合わせ
さらに高度な設計として、引数の末尾にオプション要素を持つタプルを `readonly` で保護する手法がある。
/
- APIレスポンスのパース設定
- [ステータスコード, メッセージ, …付随データ] の構造を不変に保つ
/
type ApiResponseSchema
status: number,
message: string,
…data: T
];
function logResponse
const [status, message, …rest] = response;
console.log(`[${status}] ${message}`);
// rest も自動的に readonly になり、破壊的操作は不可能
return rest;
}
// 完全に型安全かつ不変な呼び出し
logResponse([200, “Success”, { userId: 1 }, “extra_meta”] as const);
`as const` を活用することで、リテラルから直接 `ReadonlyTuple` を生成し、型推論を最大限に活用している。
—
5. アーキテクトの視点:パフォーマンスと保守性
「すべての引数を `readonly` にすると、記述量が増えて冗長ではないか?」という反論があるかもしれない。しかし、考えてみてほしい。
1. 実行時オーバーヘッドはゼロ: TypeScriptの型定義はコンパイル時に消滅する。`Readonly` を使ったからといって、JavaScriptの `Object.freeze()` のような実行時のコストは発生しない。
2. ドキュメントとしての価値: その関数が「入力を汚さない(Pureである)」ことを、型シグネチャが雄弁に語っている。
3. デバッグ時間の削減: 破壊的操作による「どこで値が変わったかわからない」という不毛な調査を、コンパイルエラーという形で未然に防いでいる。
結論
`ReadonlyTuple` を活用した引数定義は、単なる「お作法」ではない。それは、「この関数は契約を遵守し、外部の状態を汚染しない」というエンジニアとしての誠実さの証明である。
もし君がチームのテクニカルリードなら、明日からのコードレビューでこう伝えよう。
「その配列、`readonly` をつけ忘れている。君は無意識のうちに、呼び出し元のデータを破壊する権利を自分に与えてしまっているぞ」と。
型システムを掌握し、堅牢な城壁を築け。それがプロの仕事だ。