【入門編】 再帰的な型定義 – TypeScript実践ガイド

こんにちは!フロントエンド・アーキテクトの私です。
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ライフが、少しでも軽やかで楽しいものになりますように。
それでは、次の現場でお会いしましょう!

コメント

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