やあ。現場でコードを書いていて、ネストの深い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
}
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` で囲んでみるところから始めてみてほしい。何か詰まったら、いつでも相談に乗るよ。コードは正直だからね、正しく向き合えば必ず応えてくれるはずだ。

コメント