こんにちは!TypeScriptの型システムの世界へようこそ。
フルスタックエンジニアの先輩として、今日から君をワンランク上のTypeScript使いへと導いていきますね。
ここをクリアすれば、TypeScriptの基本はバッチリマスターできますよ!
さて、今回私たちが向き合うのは「関数における不変性(イミュータビリティ)の保証」です。
実務でコードを書いていると、「この関数に渡した設定オブジェクトやデータ構造を、関数内の処理でうっかり書き換えて(ミューテーション)しまって、思わぬバグを生んだ……」なんて経験、ありませんか?
TypeScriptの標準機能である `Readonly
さあ、型システムの奥深い扉を開けてみましょう!
—
1. なぜ「書き換え禁止(Readonly)」が必要なのか?
まずは、JavaScript/TypeScriptが持つ「参照渡し」の性質からおさらいしましょう。
オブジェクトや配列を関数に渡すとき、私たちは実体(メモリ上のアドレス)ではなく「参照」を渡しています。
そのため、悪気なく関数内でこんなことをしてしまうと……
type User = {
name: string;
settings: {
theme: string;
};
};
function updateTheme(user: User) {
// うっかり元のオブジェクトを書き換えてしまった!
user.settings.theme = “dark”;
}
このコード、動いてはしまいますが、呼び出し元のデータまでこっそり書き換えてしまう(サイドエフェクトの発生)ため、大規模なアプリケーションではバグの温床になります。
ここでTypeScriptの登場です。「この関数に渡すデータは、絶対に書き換えさせない!」と型でガチガチに固めてしまいましょう。
—
2. 標準の `Readonly` の限界を知る
TypeScriptには、標準でオブジェクトの直下のプロパティを読み取り専用にする `Readonly
まずはこれを使ってみましょう。
type User = {
name: string;
settings: {
theme: string;
};
};
// 標準の Readonly を適用
function printUser(user: Readonly
// これはOK(読み取り)
console.log(user.name);
// 【コンパイルエラー!】直下のプロパティは書き換えられない
// user.name = “Bob”;
// あれっ……? これはエラーにならない!?
user.settings.theme = “dark”;
}
おや? `user.settings.theme = “dark”` がエラーになりませんね。
なぜでしょうか?
実は、標準の `Readonly
実務で扱うデータは、APIレスポンスをはじめとして深くネストしていることがほとんどです。この「浅さ」の壁を突破するのが、今回本気でマスターする `DeepReadonly` です。
—
3. 降臨:すべてを凍結する `DeepReadonly`
ネストしたオブジェクトの隅々まで、すべてのプロパティに `readonly` の呪文をかけ続けるにはどうすればよいでしょうか?
ここで、TypeScriptの「条件付き型(Conditional Types)」と「再帰(Recursion)」という強力なコンパイラ機能を使います。
まずは、その魔法のコードを見てください。
// 再帰的にすべてのプロパティを Readonly にする型定義
type DeepReadonly
T extends Function ? T : // 関数型はそのまま通す
T extends object ? {
readonly [K in keyof T]: DeepReadonly
} :
T; // プリミティブ型(string, number等)はそのまま返す
このコードの仕組み(脳内トレース)
1. `T extends object`: 渡された型がオブジェクト(配列やオブジェクト)かどうかを判定します。
2. Mapped Types (`{ [K in keyof T]: … }`): オブジェクトのすべてのキー `K` を取り出し、それぞれのプロパティに `readonly` 修飾子を付与します。
3. 再帰呼び出し (`DeepReadonly
—
4. 実戦投入:`DeepReadonly` で安全な関数を書く
先ほどの `DeepReadonly` を使って、先ほどの `printUser` 関数を完璧に守ってみましょう。
// (先ほど定義した DeepReadonly
type User = {
name: string;
settings: {
theme: string;
notifications: {
email: boolean;
};
};
};
function processUserSecurely(user: DeepReadonly
// 1階層目はもちろん読み取り専用
// user.name = “Alice”; // ❌ コンパイルエラー!
// 2階層目のネストも……
// user.settings.theme = “light”; // ❌ コンパイルエラー!
// 3階層目の奥深くにあるプロパティも……!
// user.settings.notifications.email = false; // ❌ コンパイルエラー!
console.log(`Processing user: ${user.name}`);
}
const myUser: User = {
name: “Ken”,
settings: {
theme: “dark”,
notifications: { email: true }
}
};
// 安全にデータを渡せる
processUserSecurely(myUser);
どうですか? これで、関数内部でうっかりデータを破壊してしまうバグは、実行する前にTypeScriptのコンパイラが完全にシャットアウトしてくれます。
—
まとめ:型で「意図」をコードに刻もう
今回は、関数型における `Readonly` と、それを極限まで深掘りした `DeepReadonly` について解説しました。
- 標準の `Readonly
` は直下のプロパティしか守れない(浅い)。 - 自作の `DeepReadonly
` は、条件付き型と再帰を使ってネストしたオブジェクトの隅々までイミュータビリティを保証できる(深い)。
TypeScriptの型システムは、単なる「エラーチェックツール」ではありません。「この関数には、このデータ構造を絶対に改変させない」という開発者の強い意志(デザイン・インテント)をコードに刻み込むための最強の武器です。
ここをクリアした君なら、もう複雑なAPIデータ構造を扱うことも怖くないはず。
明日からの開発で、ぜひこの `DeepReadonly` を取り入れて、堅牢なコードベースを作ってみてくださいね!
それでは、次のステップでお会いしましょう。ハッピー・コーディング!