【入門編】関数型における「ReadonlyTuple」を活用した引数の不変性保証 – TypeScript コア・型システムの基礎解析バイブル

こんにちは!TypeScriptの世界へようこそ。今日は、あなたが型安全なコードを書く上で非常に強力な味方になる「ReadonlyTuple(読み取り専用タプル)」という技術についてお話しします。

プログラミングをしていると、「関数に渡した配列の中身が、知らないうちに書き換えられてバグになった……」という苦い経験をすることがありますよね。TypeScriptでは、型定義のちょっとした工夫で、そのような「意図しない破壊的な変更」をコンパイル時に100%防ぐことができるんです。

「ここをクリアすれば、TypeScriptの基本はバッチリマスターできますよ」という気持ちを込めて、優しく、かつ本質的な部分まで解説していきますね。

—

なぜ「配列の書き換え」は危険なのか?

まず、私たちが日常的に使う「配列(Array)」の性質を思い出してみましょう。

function processCoordinates(coords: number[]) {
// 座標を計算するつもりが、うっかり中身を書き換えてしまった!
coords[0] = 0;
coords.push(100);
}

const myPoint = [10, 20];
processCoordinates(myPoint);

console.log(myPoint); // [0, 20, 100] -> 元のデータが壊れてしまった!

JavaScriptの配列は、デフォルトで「可変(Mutable)」です。関数の中で`push`や`pop`をしたり、インデックスを使って直接代入したりすると、呼び出し元のデータまで変わってしまいます。

これを防ぐために「この関数の中では、引数を絶対に書き換えないでね!」と型で約束するのが、今回のテーマです。

—

1. 「ReadonlyArray」と「ReadonlyTuple」の違い

TypeScriptには、配列を読み取り専用にするための `ReadonlyArray` という仕組みがありますが、さらに一歩踏み込んで「個数や順番まで厳密に守りたい」場合に使うのが「タプル型」との組み合わせです。

タプル型とは?

タプルは「1番目は文字列、2番目は数値」のように、要素の数と型が固定された配列のことです。

// 普通の配列(長さは自由)
type StringArray = string[];

// タプル型(1番目がstring、2番目がnumberと決まっている)
type UserData = [string, number];

Readonlyを付与する

このタプルに `readonly` 修飾子をつけることで、最強のガードが完成します。

// これが「ReadonlyTuple」の基本形です
type ReadonlyPoint = readonly [number, number];

—

2. 【実践】関数でReadonlyTupleを活用する

それでは、実際に「引数の不変性を保証する関数」を書いてみましょう。GPSの座標(緯度・経度)を扱うシーンをイメージしてください。

コード例:安全な座標処理

/

  • 座標を受け取って、文字列表記に変換する関数
  • readonlyを付けることで、関数内での破壊的変更を禁止します

/
function formatLocation(location: readonly [number, number]): string {
// ✅ 読み取りは自由に行えます
const [lat, lng] = location;

// ❌ もし間違えて書き換えようとすると、TypeScriptが即座にエラーを出してくれます
// location[0] = 35.6812;
// エラー: Index signature in type ‘readonly [number, number]’ only permits reading.

// ❌ 配列を操作するメソッドも封印されます
// location.push(0);
// エラー: Property ‘push’ does not exist on type ‘readonly [number, number]’.

return `緯度: ${lat}, 経度: ${lng}`;
}

// 実行してみましょう
const point: [number, number] = [35.6586, 139.7454];
const label = formatLocation(point);

console.log(label); // “緯度: 35.6586, 139.7454”

ここがポイント!

関数の引数に `readonly [number, number]` と書くだけで、その関数の中では `push` も `pop` も、インデックスへの代入もできなくなります。

「この関数は、受け取ったデータを汚さない安全な関数ですよ」ということを、型が証明してくれるのです。

—

3. 陥りやすい文法エラーと解決策

初心者の頃に「あれ?」となりやすいポイントが2つあります。

① 普通の配列をタプルとして渡そうとする

const nums = [1, 2]; // 型は number[] (長さが決まっていない配列)
formatLocation(nums); // ❌ エラー!

解決策: `formatLocation` は「要素が2つのタプル」を求めているので、渡す側も型を明示するか、`as const` を使って固定する必要があります。

② `ReadonlyArray` と `readonly [T, T]` の混同

  • `ReadonlyArray` : 何個入っているかわからないが、読み取り専用。
  • `readonly [number, number]` : 2個しか入っていないことが保証され、かつ読み取り専用。

関数の仕様として「必ず2つの値が必要」なら、タプル形式の `readonly [T, T]` を選ぶのが正解です。

—

4. なぜこれが「プロの知恵」なのか?

私たちは人間なので、どれだけ気をつけていても「ついうっかり」コードを書き換えてしまうことがあります。特に大規模なチーム開発では、他人が書いた関数が自分のデータを書き換えてしまう恐怖が常にあります。

`readonly` を活用することで、以下のようなメリットが得られます:

1. 副作用の防止: 関数が外部の状態を壊さないことが保証される(純粋関数の維持)。
2. ドキュメントとしての型: コードを読むだけで「あ、この引数は書き換えちゃいけないんだな」と一目でわかる。
3. 安心感: コンパイラが24時間体制で見張ってくれるので、夜もぐっすり眠れる。

—

まとめ

最後に、今回学んだ「不変性を保証するステップ」を復習しましょう。

1. 複数の値をセットで扱うなら タプル型 `[T, U]` を使う。
2. 関数内で書き換えを禁止するなら、頭に `readonly` をつける。
3. `readonly [T, U]` は、長さも型も変更不可にする最強のガード。

これが使いこなせるようになれば、あなたのTypeScriptレベルは格段にアップします。データの不変性を守ることは、バグの少ない堅牢なアプリケーションへの第一歩です。

「型で守る」という感覚、少しずつ掴めてきましたか? ぜひ明日からのコードに取り入れてみてくださいね。応援しています!

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