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

TypeScriptの「不変性(Immutability)」を極める:DeepReadonlyで再帰の壁を突破しよう

こんにちは。TypeScriptの世界へようこそ。
コードを書いていると、「このオブジェクト、関数の中でうっかり書き換えられたら困るな」と感じる瞬間はありませんか?

JavaScriptのオブジェクトはデフォルトで「ミュータブル(書き換え可能)」です。しかし、大規模なアプリケーションになればなるほど、「データはどこで、誰によって変更されたのか」という追跡は悪夢になります。

今回は、TypeScriptの型システムを駆使して、ネストされた深い階層のオブジェクトまで「絶対に書き換えさせない」強力な武器、`DeepReadonly`の実装方法を伝授します。これさえマスターすれば、あなたのコードの堅牢性は一段階上のステージへ引き上げられますよ。

—

1. なぜ「Readonly」だけでは不十分なのか?

まず、TypeScript標準の `Readonly` を見てみましょう。

interface User {
name: string;
settings: {
theme: ‘light’ | ‘dark’;
};
}

// 標準のReadonlyを適用
const user: Readonly = { name: “Alice”, settings: { theme: ‘light’ } };

user.name = “Bob”; // エラー!正しく防げています。
user.settings.theme = ‘dark’; // あれ?エラーが出ない…!

お気づきでしょうか。`Readonly` は「直下のプロパティ」しか読み取り専用にしません。`settings` というオブジェクトそのものは「再代入不可」ですが、その中身(ネストされたプロパティ)までは保護できないのです。

これが、TypeScriptにおいて「浅い(Shallow)」保護と呼ばれる限界です。

—

2. 魔法の鍵:再帰型(Recursive Types)

この限界を突破するために、TypeScriptの再帰型という概念を使います。
「型の中で自分自身を呼び出し、終わりが見えるまで深掘りする」というテクニックです。

これが `DeepReadonly` の実装コードです。

type DeepReadonly = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly // オブジェクトなら、さらに奥まで再帰!
: T[P]; // プリミティブなら、そのまま固定
};

このコードの「魂」を読み解く

1. `[P in keyof T]`: オブジェクトの全キーを走査(Mapped Types)。
2. `readonly`: 全てのキーに読み取り専用修飾子を付与。
3. `T[P] extends object ? … : …`: 条件付き型(Conditional Types)。「もしその値がオブジェクトだったら?」と問いかけます。
4. `DeepReadonly`: もしオブジェクトなら、型定義の中で自分自身をもう一度呼び出します。これが再帰の正体です。

—

3. 実践:関数引数で不変性を保証する

では、この `DeepReadonly` を関数の引数に適用してみましょう。

function updateTheme(config: DeepReadonly) {
// config.settings.theme = ‘dark’;
// ↑ ここでコンパイルエラー!深い階層も完璧に保護されます。

console.log(`Current theme is: ${config.settings.theme}`);
}

const myUser: User = { name: “Alice”, settings: { theme: ‘light’ } };

updateTheme(myUser); // 安全に呼び出せます

関数に渡した瞬間に、コンパイラが「この関数内では絶対に値を変更させない」という契約(コントラクト)を強制してくれます。これぞTypeScriptの真骨頂です。

—

4. 陥りやすい罠:プリミティブと配列

ここで初学者がつまずきやすいポイントが2つあります。

その1:配列の扱い

実は `DeepReadonly` は配列にも効きます。`ReadonlyArray` と同様の効果が得られます。しかし、`Date`型や `Map`型といった「内部状態を持つ特殊なオブジェクト」は、単なる `object` と判定されず、再帰が止まってしまうことがあります。
そんな時は `T extends Function ? T : …` のように、関数を除外する条件などを加えるとより堅牢になります。

その2:型エラーが読みづらい

再帰が深すぎると、エディタのエラーメッセージが難解になることがあります。
「型が深すぎて評価できません」というエラーが出たときは、型定義が無限ループしていないか(例:循環参照)を疑ってください。

—

最後に:TypeScriptは「思考の補助輪」

`DeepReadonly` のような型を定義することは、単なる制約ではありません。「このデータは純粋であるべきだ」という設計思想を、コードという言語でチームに伝えるドキュメントそのものなんです。

最初は難しく感じるかもしれませんが、この「型を再帰させる」という感覚を一度つかめば、あなたはもうTypeScriptの初学者ではありません。

ここをクリアしたあなたは、より複雑なデータ構造を扱う準備が整いました。ぜひ、現場のコードで「守りたいオブジェクト」を見つけたら、すぐに `DeepReadonly` を適用してみてください。

また次回の講義でお会いしましょう。ハッピーコーディング!

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