【入門編】再帰的型定義(Recursive Types)でJSONツリー構造を型付けする – TypeScript コア・型システムの基礎解析バイブル

こんにちは!フロントエンドからバックエンドまで、TypeScriptの深淵を日々模索している先輩エンジニアです。

今回は、TypeScriptの型システムが持つ「美しさ」と「奥深さ」を存分に味わえるテーマ、「再帰的型定義(Recursive Types)によるJSONツリー構造の型付け」について解説していきますね。

「オブジェクトの中にさらにオブジェクトがあって、その中にも……」というネストしたデータ、実務の開発でも本当によく見かけますよね。これを適当な `any` で済ませてしまうのは簡単ですが、それではTypeScriptを使う醍醐味が半減してしまいます。

ここをクリアすれば、あなたも型の構造を自由自在に操るモデラーの仲間入りです。一緒に一歩ずつ、しっかりとマスターしていきましょう!

—

1. なぜ「再帰的型定義」が必要なのか?

皆さんは、Web APIから以下のようなJSONデータを受け取ったことはありませんか?

{
“name”: “root”,
“children”: [
{
“name”: “child-1”,
“children”: [
{ “name”: “grandchild-1”, “children”: [] }
]
}
]
}

このデータは、親の中に子(`children`)が配列として入り、その子の中にもさらに孫が……という、まるでロシアの民芸品「マトリョーシカ」のようなツリー構造をしています。階層の深さが「最大3階層まで」と決まっていれば手動で型を書けますが、「何階層続くか分からない」としたらどうでしょう?

ここで登場するのが、自分自身を型の中で呼び出す「再帰的型定義(Recursive Types)」です。

—

2. 基本的なアプローチ:Type Aliasで作るJSONツリー

まずは、最も基本となるJSONのプリミティブ型(文字列、数値、真実値、null)を表現しつつ、オブジェクトと配列を再帰的に定義してみましょう。

プログラミング初心者の方は、「型が自分自身を指すなんてできるの?」と驚かれるかもしれませんが、TypeScriptの `type`(型エイリアス)ならそれが可能です。

// 1. JSONで扱える基本的な値の型
type JsonPrimitive = string | number | boolean | null;

// 2. JSONオブジェクトの型(自分自身をプロパティに含む)
type JsonObject = {
[key: string]: JsonValue;
};

// 3. JSON配列の型(自分自身を要素に含む)
type JsonArray = JsonValue[];

// 4. これらをすべて統合した「JSONの全体像を表す型」
type JsonValue = JsonPrimitive | JsonObject | JsonArray;

// — 実際の使い方 —
const myData: JsonValue = {
id: 1,
title: “TypeScriptの世界”,
metadata: {
author: “先輩エンジニア”,
tags: [“ts”, “javascript”, “frontend”],
isPublished: true,
},
children: null,
};

このコードの意味と仕組み

  • `JsonValue`: 文字列や数値といった「これ以上分解できない型(Primitive)」から、オブジェクトや配列といった「中にさらに値を持つ型」までをすべて網羅しています。
  • 相互再帰(Mutually Recursive Types): `JsonObject` の中にある `JsonValue` が、巡り巡って再び `JsonObject` や `JsonArray` を呼び出す構造になっています。これが、無限の深さを持つJSONを型安全に受け止められる秘密です。

—

3. 実践!深さ無限の「ファイルツリー構造」を型付けする

もう少し具体的に、ファイルシステムのような「名前(name)」と「子要素(children)」を持つツリー構造を例に、エディタの強力な補完(IntelliSense)がどう効くかを見てみましょう。

// ファイルシステムを模した再帰的型定義
type FileNode = {
name: string;
type: ‘file’ | ‘directory’;
size?: number; // ファイルの場合のみ存在することを想定
children?: FileNode[]; // ディレクトリの場合、自分自身の配列を持つ!
};

// 型の恩恵を受けたツリーデータの構築
const projectRoot: FileNode = {
name: “my-app”,
type: “directory”,
children: [
{
name: “package.json”,
type: “file”,
size: 1200,
},
{
name: “src”,
type: “directory”,
children: [
{
name: “index.ts”,
type: “file”,
size: 450,
},
],
},
],
};

ここで `projectRoot.children?.[1].children?.[0].name` とアクセスしてみてください。
何階層深くアクセスしても、TypeScriptのコンパイラは `name` が `string` 型であることを完璧に追跡し、エディタが自動補完を出してくれますよね。これが型定義の力です。

—

4. 陥りやすい罠と文法エラー

再帰的型定義を書き始めると、多くの開発者が一度はハマる「落とし穴」があります。代表的なものを2つご紹介しますね。

罠その1:`interface` で再帰させようとして失敗する

「オブジェクトだから `interface` を使おう!」と思って、以下のように書いたことはありませんか?

// ❌ 以下のコードはコンパイルエラーになることがあります
interface BadNode {
name: string;
children: BadNode[];
}

実は、最近のTypeScriptでは `interface` でも自己参照(再帰)ができるようになっていますが、複雑な複合型(ユニオン型など)を挟んだ再帰を行う場合や、型の評価順序によってはコンパイラが「型を解決できません」と悲鳴を上げることがあります。

【解決策】
迷ったら、型の柔軟性と再帰の相性が抜群な `type`(型エイリアス) を使うのが、安全でスマートなベストプラクティスです。

罠その2:無限ループ(無限に深い型)によるコンパイラ泣かせ

再帰型を書くときに、「終了条件(ベースケース)」を忘れて無限にネストし続けるような定義にすると、TypeScriptのコンパイラ(tsc)がスタックオーバーフローを起こすか、型推論が諦められて `any` に落ちてしまいます。

// ❌ 終了条件や実体への逃げ道がない危険な再帰
type InfiniteType = {
next: InfiniteType;
};

必ず、プリミティブ型やオプショナル(`?`)、あるいは配列の終端を許容する構造(今回でいう `JsonPrimitive` や `undefined`)を挟むように意識しましょう。

—

5. おわりに

いかがだったでしょうか?
「再帰的型定義」と聞くと何やら難しそうなアルゴリズムを想像してしまいますが、本質は「自分自身の構造の中に、自分自身をもう一度組み込む」という非常にシンプルなルールです。

ここをクリアできると、複雑なAPIレスポンス、Reduxなどのステートツリー、AST(抽象構文木)の解析など、フロントエンドの高度な領域で必要となるデータ構造を完全に手なずけることができるようになります。

「自分の書いたデータ構造の型が、どこまでも深く、優しく補完してくれる」――この感動をぜひ実際の開発でも味わってみてくださいね。TypeScriptの基本は、もうバッチリマスターできていますよ!

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