こんにちは。TypeScriptの深淵を覗き込み、その型システムの美しさに魅せられた皆さん、ようこそ。
今回は、TypeScriptにおける「再帰的型定義(Recursive Types)」という、一見すると魔法のように見えるけれど、実は型システムの基礎体力を鍛えるには最高のトピックを扱います。
DOMやJSONのように、自分の中に自分を持つ「ツリー構造」。これをただの `any` で済ませるか、それとも強固な型で守り抜くか。ここを理解すれば、あなたの書くコードは「動くもの」から「壊れないもの」へと劇的に進化します。
—
1. 再帰的型定義とは何か?
プログラミングにおいて、自分自身の定義の中で自分を参照するデータ構造のことを「再帰的構造」と呼びます。
例えば、フォルダの中にフォルダがあり、その中にまたフォルダがある……といった構造ですね。これをTypeScriptで表現するには、`interface` よりも `type` (型エイリアス) を使うのが定石です。
まずは、シンプルな「ノード」の型を定義してみましょう。
type Node = {
value: string;
// childrenはNodeの配列。自分自身を指しているのがポイントです
children?: Node[];
};
const tree: Node = {
value: “root”,
children: [
{ value: “child-1” },
{ value: “child-2”, children: [{ value: “grandchild” }] }
]
};
ここで重要なのは、「型が未解決のまま自分自身を参照できる」というTypeScriptの柔軟な仕様です。これによって、無限に続く深い階層構造を静的に記述できるようになります。
—
2. なぜ `interface` ではなく `type` なのか?
初心者の頃は「`interface` と `type` どっちを使えばいいの?」と迷うはずです。結論から言うと、再帰的な型定義には `type` を選んでください。
もちろん `interface` でも再帰は可能ですが、複雑なジェネリクスや条件付き型(Conditional Types)と組み合わせる際、`type` の方が圧倒的に表現力が高いからです。
- interface: オブジェクト指向的な拡張(`extends`)が得意。
- type: 合併型(`|`)や交差型(`&`)、そして複雑な再帰構造の定義が得意。
ツリー構造を扱うときは、型の中に「ここにはこれが入る、あるいはこれが入る」という条件分岐的な論理を組み込むことが多いため、`type` が圧倒的に有利なのです。
—
3. 型安全にツリーを走査する(再帰関数の型付け)
型を定義するだけでなく、実際にそのツリーを走査する関数を書いてみましょう。ここで TypeScript の「型推論」の真価が発揮されます。
/
- ツリーの全ノードの値をコンソールに出力する関数
/
function traverse(node: Node): void {
console.log(node.value);
// childrenが存在する場合のみ、再帰的に呼び出す
if (node.children) {
for (const child of node.children) {
traverse(child);
}
}
}
このコード、実は非常に優秀です。`node.children` を `if` でチェックすることで、TypeScript のコンパイラは「このブロックの中では `children` は必ず存在する」と判定します。これをタイプガード(Type Guard)と呼びます。
—
4. 陥りやすい罠:無限再帰と型エラー
再帰型を書いていると、時々コンパイラに「型が深すぎて評価できません」と怒られることがあります。あるいは、再帰が止まらずに実行時エラー(スタックオーバーフロー)を起こすことも。
陥りやすいエラーの例
// 悪い例:再帰の終了条件が曖昧
type BadNode = {
next: BadNode; // これだと「無限の連鎖」が確定してしまい、型定義自体が崩壊します
};
解決策: 必ず「省略可能(`?`)」や「終端(`null` や `undefined`)」を許容するようにしましょう。再帰は「いつか必ず終わる」という保証が必要です。
—
5. ステップアップ:ジェネリクスで汎用化する
「文字列だけじゃなくて、数字のツリーも扱いたいな」と思ったら、ジェネリクス(Generics)の出番です。型をパラメータ化してしまいましょう。
type Tree
value: T;
children?: Tree
};
// これでどんな型でもツリー構造にできる!
const numberTree: Tree
value: 1,
children: [{ value: 2 }]
};
ここまでくれば、あなたはもうTypeScriptの再帰構造を完全に掌握したと言えます。
—
最後に:TypeScriptの「深さ」を楽しもう
再帰的な型定義は、一見すると難解ですが、「データの構造を言語化する」というプログラミングの本質に触れる部分です。
最初は `interface` や `type` の使い分けに迷うこともあるでしょう。でも大丈夫です。コンパイラがエラーを出してくれるのは、あなたのコードがより良くなるための「ヒント」をくれているだけなのですから。
まずは手元のJSONデータを、この `type Tree
それでは、良い型ライフを!