こんにちは、皆さんの成長を心から応援しているシニアアーキテクトです。
TypeScriptの世界へようこそ。型システムを学び始めると、誰もが一度は「このデータ、どこまで深く続くかわからないな……」という壁にぶつかります。例えば、フォルダ構造やJSONデータのように、自分自身の中に自分自身が含まれるような構造ですね。
今日は、TypeScriptの基礎の中でも特に「奥が深い」とされる、型エイリアスにおける再帰的定義についてお話ししましょう。ここをマスターすれば、TypeScriptで表現できないデータ構造はほとんどなくなりますよ。
—
1. 再帰的な型とは?(マトリョーシカのイメージ)
「再帰」という言葉を聞くと難しく感じるかもしれませんが、イメージは簡単です。ロシアの民芸品「マトリョーシカ」を思い出してください。人形を開けると、中からまた人形が出てきますよね?
プログラミングにおける再帰的な型も同じです。
「自分自身の定義の中に、自分自身が登場する型」のことを指します。
もっとも身近な例は、JSON構造です。
// シンプルなJSON値のイメージ
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[] // ← ここで自分自身(JSONValue)を呼び出している!
| { [key: string]: JSONValue }; // ← ここでも自分自身を呼び出している!
このように、配列の中身やオブジェクトの値が、また「JSONValue」であるという定義。これが再帰的定義の基本です。
—
2. かつてのTypeScriptが抱えていた「限界」
実は、昔のTypeScript(バージョン3.6以前)では、上記のような「型エイリアス(`type`)」を直接再帰させることには厳しい制限がありました。
昔は、以下のようなコードを書くと「型エイリアス ‘JSONValue’ が循環的に自身を参照しています」というエラーが出て、コンパイラに怒られてしまったのです。
なぜエラーになっていたのか?
TypeScriptのコンパイラは、型を評価する際に「中身をすべて展開」しようとします。もし無限に自分を呼び出していると、コンパイラの脳みそ(メモリ)がパンクしてしまうからです。
当時は、この制限を回避するために `interface` を使って「一呼吸置く」必要がありました。
// 昔の解決策:interfaceは「遅延評価」される性質を利用する
interface JSONObject {
[key: string]: JSONValue;
}
interface JSONArray extends Array
type JSONValue = string | number | boolean | null | JSONObject | JSONArray;
—
3. 現代のTypeScript(3.7以降)がもたらした革命
今の皆さんはとても幸運です!TypeScript 3.7で「再帰的型エイリアス」の制限が大幅に緩和されました。
コンパイラが賢くなり、「あ、これは再帰しているな。必要になった時にだけ中身を評価しよう」と遅延評価(Lazy Evaluation)をしてくれるようになったのです。
これにより、先ほど書いた直感的なコードがそのまま動くようになりました。
/
- 現代のスマートなJSON定義
- これだけで、無限に深いネストも型安全に扱えます。
/
type Json =
| string
| number
| boolean
| null
| { [key: string]: Json } // 直感的に自分を呼べる
| Json[]; // 配列の要素としても自分を呼べる
const data: Json = {
name: “TypeScript”,
features: [“Static Typing”, “Inference”],
details: {
isAwesome: true,
versions: [3.7, 4.0, 5.0]
}
};
—
4. ここで差がつく!陥りやすい「無限ループ」の罠
基本はマスターできましたね。ですが、アーキテクトとして皆さんに注意してほしい「落とし穴」があります。それは「終了条件のない直接的な再帰」です。
以下のコードを見てください。
// ❌ 危険:コンパイルエラーになります
type Infinite = {
next: Infinite;
};
// ❓ どうやってこの変数を作りますか?
// const loop: Infinite = { next: { next: { next: … } } };
これは「マトリョーシカの中に、必ずマトリョーシカが入っていなければならない(中身が空の人形が存在しない)」という定義です。これではいつまで経っても実体を作ることができず、TypeScriptは「循環参照が複雑すぎます」と警告を出します。
解決策:必ず「出口」を作る
再帰的な型を定義するときは、必ず再帰しない選択肢(プリミティブ型など)を混ぜてあげてください。
// ✅ 安全:出口がある
type SafeNode = {
value: number;
next: SafeNode | null; // null という「出口」がある
};
—
5. 実践:ツリー構造を定義してみよう
最後に、現場でよく使う「木構造(Tree)」を定義してみましょう。ファイルシステムのディレクトリ構造などをイメージすると分かりやすいですよ。
/
- ファイルシステムの型定義
/
type FileNode = {
name: string;
// typeが ‘file’ の場合は中身(content)を持ち、
// ‘directory’ の場合はさらに子要素(children)を持つ
type: ‘file’ | ‘directory’;
content?: string;
children?: FileNode[]; // ここで再帰!
};
const myProject: FileNode = {
name: “my-app”,
type: “directory”,
children: [
{
name: “src”,
type: “directory”,
children: [
{ name: “index.ts”, type: “file”, content: “console.log(‘Hello’);” }
]
},
{ name: “package.json”, type: “file”, content: “{}” }
]
};
このコードの素晴らしいところは、`children` の中に入れたデータも、自動的に `FileNode` として型チェックされることです。もし `children` の中の `type` を打ち間違えたら、TypeScriptが即座に教えてくれます。
—
まとめ:TypeScriptを掌握するための第一歩
再帰的定義を理解することは、TypeScriptの「柔軟性」と「厳密さ」のバランスを理解することに他なりません。
1. 再帰的型エイリアスは、複雑なネスト構造を定義する強力な武器。
2. 現代のTypeScript(3.7〜)なら、`type` を使って直感的に書ける。
3. ただし、必ず `null` やプリミティブ型などの「出口」を用意すること。
ここをクリアできれば、あなたはもう初心者ではありません。データの構造を型で正確に記述できる、立派な型職人の入り口に立っています。
「型が自分を呼んでいる」——その構造の美しさを楽しみながら、コードを書いてみてくださいね。もし分からなくなったら、いつでもこのマトリョーシカの話を思い出してください。
次は、この再帰を応用した「条件付き型(Conditional Types)」の世界でお会いしましょう。応援していますよ!