【実務・中級編】再帰的型定義(Recursive Types)で表現する「ツリー構造」の型安全な走査 – TypeScript コア・型システムの基礎解析バイブル

型の深淵を歩く:再帰的データ構造を「コンパイル時」に制圧する技術

多くのエンジニアが、再帰的なデータ構造(DOMツリー、AST、階層化されたJSON)を前にして、`any` や `unknown` を安易にキャストして逃げている。だが、それは型安全性の放棄であり、将来のバグへの招待状だ。

TypeScriptの型システムは、再帰を許容する強力なエンジンを持っている。本稿では、このエンジンを正しく回し、再帰的構造を「型安全」に走査するための設計思想を伝授する。

—

1. 「自己参照型」の正しい定義と限界

まず、最も基本的なツリー構造の定義を見てほしい。

type TreeNode = {
value: T;
children?: TreeNode[];
};

一見完璧に見える。しかし、実務においてこの定義には大きな罠がある。「型推論の深さ」によるコンパイラの悲鳴だ。TypeScriptの型チェッカーは、再帰が深すぎると「型が複雑すぎる」と判断し、`any` にフォールバックするか、計算を放棄する。

これを回避し、かつ柔軟に扱うために、再帰の「末端」を明示的に制御する設計が重要になる。

—

2. 「Discriminated Union」による堅牢なツリー設計

単なる `children` の配列ではなく、ノードの種類を明確に定義する「直和型(Discriminated Union)」を使うのが、プロダクションコードの定石だ。これにより、走査時の `if` 文や `switch` 文が型ガードとして機能する。

type FileNode = { type: ‘file’; name: string; size: number };
type FolderNode = { type: ‘folder’; name: string; children: FileSystemNode[] };

type FileSystemNode = FileNode | FolderNode;

/

  • 再帰的に走査し、合計サイズを算出する
  • コンパイラは各分岐で ‘type’ を見ているため、安全にプロパティへアクセスできる

/
function calculateTotalSize(node: FileSystemNode): number {
if (node.type === ‘file’) {
return node.size;
}

return node.children.reduce((acc, child) => acc + calculateTotalSize(child), 0);
}

ここがポイント:
TypeScriptは、`node.type === ‘file’` という条件式だけで、そのブロック内での `node` を `FileNode` 型に「絞り込み(Narrowing)」する。これにより、`node.children` にアクセスしようとするとコンパイルエラーになるため、バグを未然に防げる。

—

3. ジェネリクスと「マッピング」を活用した走査の一般化

実務では、ツリーを「変換(Map)」したいケースが頻発する。例えば、APIから取得したツリーをUIコンポーネント用の構造にマッピングする場合だ。これを毎回手書きするのは非効率かつ危険だ。

以下は、型安全性を維持しつつ再帰的に変換をかける汎用的なパターンだ。

type Mapper = (node: T) => R;

function mapTree(
node: T,
mapper: (n: T, recurse: (child: T) => R) => R
): R {
const recurse = (child: T) => mapTree(child, mapper);
return mapper(node, recurse);
}

// 使用例:ファイルサイズを文字列に変換する例
const tree: FileSystemNode = { / … / };

const stringSizeTree = mapTree(tree, (node, recurse) => {
if (node.type === ‘file’) return `${node.size}KB`;
return node.children.map(recurse).join(‘, ‘);
});

このアプローチの利点は、「走査のロジック」と「変換のロジック」が分離されていることだ。これにより、再帰の深さや順序といった計算の責務をライブラリ側に任せ、ビジネスロジックに集中できる。

—

4. パフォーマンスと型評価の注意点

最後に、アーキテクトとして忠告しておく。

1. 無限再帰の回避: 再帰的型定義を定義する際、必ず `undefined` や `null`、あるいは `base case` を含めること。さもなくば、コンパイラが「型定義の解釈に失敗」して、IDEが重くなる。
2. 型エイリアスの多用は避ける: 再帰が極端に深い場合、`type` よりも `interface` を使ったほうが、TypeScriptの型チェック速度が向上することがある(`interface` は名前付きでキャッシュされやすいため)。
3. スタックオーバーフロー: JavaScriptの実行エンジン(V8)は、再帰の深さに限界がある。DOMツリーが数万要素を超えるような極端なケースでは、再帰的な関数ではなく、スタックデータ構造を用いた「ループによる走査」に書き換える勇気を持つこと。

結び:型は「ドキュメント」以上の存在である

再帰的型定義をマスターするということは、データの「形」をコンパイラに理解させるということだ。あなたが書いた型定義は、未来の自分やチームメンバーに対する最強のドキュメントであり、同時にランタイムエラーを防ぐ最強の防波堤になる。

「なんとなく動く」コードから卒業し、「型が保証する正しい構造」を設計するエンジニアへ。その一歩が、プロダクトの寿命を確実に伸ばす。

コードを信じるな、型を信じろ。そして、その型が指し示す先にある、計算の本質を見極めよ。

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