こんにちは!フロントエンド・アーキテクトの私です。
TypeScriptを触りはじめると、「あれ、これってどう型をつけたらいいんだっけ?」と手が止まる瞬間、ありますよね。特に、JSONデータやツリー構造のように、どこまで深くなるか分からないデータを扱うとき、「うわ、難しそう…」とそっとブラウザを閉じたくなる気持ち、痛いほどよく分かります。
でも、大丈夫ですよ。安心してください。
今日は、そんな複雑なデータをスッキリ片付ける魔法のようなテクニック、「再帰的な型定義(さいきてきなかていぎ)」について、身近な例えを交えながらゆっくりとお話ししていきますね。
—
1. 再帰的ってなぁに? マトリョーシカ人形を思い出そう
いきなり「再帰(Recursion)」なんて言葉を聞くと、エンジニアの要塞に迷い込んだようなプレッシャーを感じるかもしれませんが、難しく考える必要はありません。
身近なもので例えるなら、「マトリョーシカ人形」や、お母さんがお買い物に行くときの「おつかいメモ」をイメージしてください。
例えば、スーパーで「りんごを買う」というタスクがあったとします。でも、もしそのりんごが売り切れていたら、「みかんを買う」。もしみかんもなかったら、「バナナを買う」……というように、「同じ構造のルールが、自分のなかに繰り返しネストして入っている状態」のことを、プログラミングの世界では「再帰」と呼びます。
Web制作の現場でも、
- フォルダの中にフォルダがあって、その中にさらにフォルダがある(階層構造)
- ネットショップの「カテゴリ」の中に「子カテゴリ」があり、その中にまた「孫カテゴリ」がある
こういう「どこまで深くなるか予測がつかないデータ」をTypeScriptでピタッと表現したいときに、再帰的な型定義がめちゃくちゃ良い仕事をしてくれるんです。
—
2. まずは身近な「メニューリスト」でイメージをつかもう
百聞は一見にしかず。まずは、Webサイトによくある「ドロップダウンメニュー」を想像してみてください。
親メニューの中に子メニューがあり、子メニューの中にさらに孫メニューがあるかもしれない、あの構造です。
これをTypeScriptでどう書くか、一緒にコードを見ていきましょう。
// メニュー項目の型を定義してみよう
type MenuItem = {
name: string; // メニューの文字(例: “ホーム”, “製品情報”)
url: string; // リンク先
// ここがポイント!自分自身(MenuItem)のリストが入るかもしれない
children?: MenuItem[];
};
ね? 意外とシンプルでしょう?
`children?: MenuItem[];` の部分に注目してください。「メニュー(MenuItem)の中に、小さなメニューたちの配列(MenuItem[])が入ってもいいよ」と定義しているだけなんです。
これが、型が自分自身を呼び出す「再帰的な型定義」の基本の形です。
—
3. 実践!深さ無限大の「コメントツリー」を作ってみよう
もう少し実務っぽい例に行きましょう。
ブログのコメント欄を想像してください。誰かのコメントに対して「返信」がつき、その返信に対してさらに「返信」がつく……いわゆるスレッド形式のコメント機能です。
このデータ構造をTypeScriptの型でビシッと縛ってみます。エディタを開くつもりで、以下のコードを読んでみてください。
// コメントの型定義
type CommentItem = {
id: number;
author: string; // 投稿者の名前
content: string; // コメント本文
// 返信コメントたち(この中身も、まったく同じ CommentItem の構造を持つ!)
replies?: CommentItem[];
};
// 実際にこの型に沿ったデータを作ってみるよ
const rootComment: CommentItem = {
id: 1,
author: “田中さん”,
content: “このTypeScriptの記事、すごく分かりやすいです!”,
replies: [
{
id: 2,
author: “鈴木さん”,
content: “本当にそうですね!”,
replies: [
{
id: 3,
author: “田中さん”,
content: “ですよねー!一緒に頑張りましょう!”,
// ここにさらに replies を繋げることもできる(無限の深さに対応!)
}
]
},
{
id: 4,
author: “佐藤さん”,
content: “私もそう思います。”
// replies がなくても、オプショナル(?)なのでエラーにならないよ
}
]
};
どうですか?
`CommentItem` という型の中に、また `CommentItem` の配列である `replies` が含まれています。これにより、データベースから何階層のデータが返ってきても、TypeScriptが「よし、型通りだな!」と優しく見守ってくれるようになります。
—
4. 初学者がハマりがちな「つまずきポイント」と処方箋
再帰的な型を書きはじめると、多くの人が一度は次のような壁にぶつかります。でも、大丈夫。みんな通る道ですからね。
つまずき①:どこで終わるの?(無限ループの恐怖)
「自分を呼び出す」ということは、うっかりすると「終わりのない無限の迷宮」に入り込んでしまう気がして怖くなりますよね。
【安心の処方箋】
大丈夫です。オプショナルチェイニング(`?`)を使って、`replies?: CommentItem[];` のように「なくてもいい(ここで終わり)」という逃げ道を作っておけば、TypeScriptは無限ループでフリーズしたりはしません。必ず「終点(葉)」を作れるように、子要素はオプショナル(省略可能)にしておくのが実務の知恵です。
つまずき②:関数でどうやって扱うの?(再帰関数とのコンビネーション)
型を定義したはいいものの、実際にこのデータを画面に表示するとき(コンポーネントで描画するときなど)、どうやって処理すればいいか迷いませんか?
型が再帰的なら、それを処理する関数も「自分自身を呼び出す関数(再帰関数)」にしてあげると、パズルのように綺麗にハマります。
// コメントツリーをぜんぶ画面に表示する(イメージ用の関数)
function printComments(comment: CommentItem, depth: number = 0) {
// 階層に合わせてインデント(スペース)をつけるよ
const indent = ” “.repeat(depth);
console.log(`${indent}- ${comment.author}: ${comment.content}`);
// もし返信(replies)があれば、それぞれのコメントに対してこの関数をもう一度呼ぶ!
if (comment.replies) {
for (const reply of comment.replies) {
printComments(reply, depth + 1); // 階層を1つ深くして自分を呼ぶ
}
}
}
// 実行してみよう!
printComments(rootComment);
このコードを実行すると、コンソールには階層構造が綺麗にインデントされて出力されます。型と関数、両方で「再帰」の呼吸を合わせることで、複雑なツリー構造もスルスルと解きほぐすことができるんです。
—
さいごに:難しく考えず、まずは「入れ子」を楽しもう
再帰的な型定義、いかがでしたでしょうか?
最初は「自分を呼び出す」という概念にフリーズしてしまうかもしれませんが、身の回りの「入れ子構造(マトリョーシカやフォルダ)」を思い出しながら書けば、怖くありません。
TypeScriptは、私たちの書いたコードの意図を汲み取り、未来のバグから守ってくれる心強い相棒です。「ここは階層が深くなるかも」と思ったら、ぜひ今回の記事を思い出して、恐れずに `typehoge = { children?: typehoge[] }` と書いてみてくださいね。
あなたのTypeScriptライフが、少しでも軽やかで楽しいものになりますように。
それでは、次の現場でお会いしましょう!

コメント