【実務・中級編】 再帰的型定義 (Recursive Types) – TypeScript実践ガイド

やあ。現場でコードを書いていて、ネストの深いJSONデータや、ディレクトリ構造のような「終わりが見えない構造」に頭を抱えたことはないかな?

フロントエンドでAPIから受け取るデータは、時として残酷なほど複雑だ。今日は、そんな「再帰的型定義(Recursive Types)」という、TypeScriptにおける一つの芸術であり、同時に諸刃の剣でもある技術について話そうと思う。

これをマスターすれば、君の型定義は「とりあえずanyで逃げる」という恥ずかしい状態から、堅牢で美しいアーキテクチャへと一気に進化するはずだ。

—

なぜ「再帰」が必要なのか?

TypeScriptで再帰的型定義を必要とするシーンは、主に「型定義の深さが予測できないとき」だ。

例えば、UIライブラリのメニュー構造や、ファイルシステムのツリー構造、あるいはバックエンドから降ってくる「コメントの親子関係」など。これらは、親の中に子がいて、その子の中にまた子がいる。この構造を `interface` で無限に書き連ねることは不可能だよね。

ここで登場するのが、自分自身を参照する型定義だ。

現場で即戦力になる「JSONツリー」の例

まずは、最もよくあるケースである「JSONライクなツリー構造」を見てみよう。

/

  • JSONのような値の型定義
  • プリミティブな型と、それらが再帰的に入るオブジェクト・配列を定義する

/
type JSONValue =
| string
| number
| boolean
| null
| { [key: string]: JSONValue } // ここで自分自身を参照!
| JSONValue[]; // 配列の中にも自分自身を許可

const myData: JSONValue = {
id: 1,
meta: {
tags: [“typescript”, “recursive”],
active: true,
},
children: [
{ title: “Sub-item 1” },
{ title: “Sub-item 2”, children: [{ title: “Deep-nest” }] }
]
};

この型定義の肝は、`{ [key: string]: JSONValue }` の部分にある。TypeScriptは、この型が確定する前に「あ、`JSONValue`って名前はさっき出てきたな」と認識し、再帰的に型を解決してくれるんだ。

—

裏側で何が起きているのか?(コンパイラの苦悩)

ここで少しだけ、TypeScriptコンパイラ(tsc)の深淵を覗いてみよう。

TypeScriptは、型を解決する際に「型をどんどん展開していく」という処理を行う。しかし、再帰的型定義は一歩間違えると「無限ループ」に陥ってしまう。コンパイラが「この型はどこまで展開すれば終わるんだ?」と迷子になると、例の `Type instantiation is excessively deep and possibly infinite.` というエラーが出るわけだ。

実は、ブラウザがJavaScriptを実行する段階では、これらの「型」はすべて消滅している(Type Erasure)。つまり、再帰的型定義はランタイム(実行時)の負荷を一切生まない。

これはフロントエンドエンジニアにとって最大のメリットだ。複雑な型を書いても、ブラウザのメモリを食うことはない。コンパイル時のチェックコストはかかるが、それは開発者のPCやCIサーバーが負担する。実行時のユーザー体験を守るために、型定義で苦労するのは非常に投資対効果が高い行為だと言える。

—

現場で差がつく!「連結リスト」の実践

ツリー構造だけでなく、連結リスト(Linked List)のような構造も、TypeScriptで表現すると非常にエレガントだ。

/

  • 連結リストのノード定義
  • nextプロパティが自分自身を持つか、あるいは空(null)であることを示す

/
interface ListNode {
value: T;
next: ListNode | null;
}

const list: ListNode = {
value: “First”,
next: {
value: “Second”,
next: {
value: “Third”,
next: null // 終端
}
}
};

これを活用すれば、複雑な状態管理や、独自のデータ構造を構築する際、型安全性を維持したまま実装を進められる。特に `T` をジェネリクスにすることで、再利用性が跳ね上がる点に注目してほしい。

—

守るべき「3つの掟」

最後に、現場で再帰的型定義を扱う際の「掟」を授けておく。

1. 必ず「終了条件」を作る:
再帰の中には必ず、`null` や `undefined` 、あるいは空の配列など、再帰をストップさせる「停止条件」を組み込むこと。これが漏れるとコンパイラが悲鳴を上げる。
2. `any` で逃げない:
「深すぎて型が書けないから `any` にしよう」という判断は、将来の自分に対する裏切りだ。一度 `any` を入れると、その後のプロパティアクセスはすべて型チェックの対象外になり、バグの温床になる。
3. 複雑すぎる場合は `type` を活用する:
`interface` よりも `type` の方が、再帰的な定義や複雑な合成(Union型など)と相性が良い場合が多い。迷ったら `type` を使って定義を整えよう。

—

再帰的型定義は、一見すると魔法のように見えるかもしれない。だが、これは「データ構造の本質を型として記述する」という、エンジニアとしての基礎体力を磨くための最高の練習台だ。

恐れずに、まずは手元の複雑なデータ構造を `type` で囲んでみるところから始めてみてほしい。何か詰まったら、いつでも相談に乗るよ。コードは正直だからね、正しく向き合えば必ず応えてくれるはずだ。

コメント

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