TypeScriptの「Type instantiation is excessively deep」エラーの解決方法

advanced🔵 TypeScript2026-05-18| TypeScript 3.4+, Node.js, VS Code, tscを使用したCI/CDパイプライン

Error Message

Type instantiation is excessively deep and possibly infinite.
#typescript#generics#recursive-types#type-depth

問題点このエラーは通常、深いオブジェクトのフラット化、カスタムORMクエリビルダー、ネストされたステートマッパーなど、野心的なものを構築しているときに発生します。突然、TypeScriptコンパイラが処理を断念します。IDEでこのエラーが点滅したり、CI/CDパイプラインが停止したりするのを目にすることになります。

Type instantiation is excessively deep and possibly infinite.

TypeScriptコンパイラは、パフォーマンスを維持するために組み込みの再帰制限を使用しています。一般的に、この制限は50レベル程度の深さに設定されています。明確な終了条件なしにジェネリック型が自分自身を何度も呼び出しすぎると、コンパイラはプロセスを強制終了します。これにより、CPU使用率が100%に達して開発環境全体がハングアップするのを防いでいます。

なぜこれが発生するのかコンパイラが型を十分に速く解決できない場合、このセーフティブレーキが作動します。主に以下の3つのシナリオで遭遇する可能性が高いです。

  • 深いデータツリー: レガシーなCMSからの再帰的なJSON構造や、複雑なディレクトリツリーを処理する場合。- 巨大なUnion型: 50〜100以上のメンバーを持つUnion型を反復処理するマップ型(Mapped Types)を使用する場合。- 循環参照: 型Aが型Bを参照し、型Bが型Aを指し示すことで、コンパイラがフラット化できないループが発生する場合。## 実証済みの修正策### 1. インターフェースを使用して評価を遅延させる型エイリアス(type)は先行評価(Eager)です。コンパイラはそれらを即座に解決しようとします。しかし、インターフェース(interface)は遅延評価(Lazy)です。ロジックの再帰部分をインターフェース内にラップすることで、深度カウンターがリセットされることがよくあります。コンパイラは、特定のプロパティを実際に使用するまで、構造全体を解決しようとしません。
// ❌ Tが深くネストされている場合、エラーを誘発します
type DeepWrap<T> = {
  data: T;
  inner: DeepWrap<T>;
};

// ✅ インターフェースを使用すると遅延評価が強制されます
interface DeepWrap<T> {
  data: T;
  inner: DeepWrap<T>;
}

2. 手動の深度カウンターを実装するDeepReadonlyのようなユーティリティ型を構築する場合、「燃料(fuel)」パラメータを渡すことで再帰を制限できます。タプルベースのカウンターを使用して、コンパイラが何レベル進んだかを追跡します。燃料がなくなると、再帰は停止します。

type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

type DeepResolve<T, D extends number = 5> = 
  D extends 0 
    ? T 
    : T extends object 
      ? { [K in keyof T]: DeepResolve<T[K], Prev[D]> }
      : T;

// これは5レベルの深さまでしか再帰せず、コンパイラのクラッシュを防ぎます
type SafeType = DeepResolve<MyComplexObject, 5>;

3. 末尾再帰最適化(TCO)を活用するTypeScript 4.5では、条件付き型の末尾再帰が導入されました。再帰呼び出しがロジック内の完全に最後の操作である場合、コンパイラはそれを最適化できます。これにより、制限が約50レベルから1,000レベル近くまで引き上げられます。

// ❌ 末尾再帰ではない:スプレッド展開が再帰の「後」に発生している
type Reverse<T extends any[]> = T extends [infer Head, ...infer Tail]
  ? [...Reverse<Tail>, Head]
  : [];

// ✅ 末尾再帰:再帰が最後の操作である
type ReverseTCO<T extends any[], Acc extends any[] = []> = T extends [infer Head, ...infer Tail]
  ? ReverseTCO<Tail, [Head, ...Acc]>
  : Acc;

4. 「any」による脱出ハッチサードパーティのライブラリや、リファクタリングできないレガシーコードに悩まされることがあります。このような場合は、中間セグメントをanyにキャストすることで再帰チェーンを断ち切ります。強引な手法ですが、無限ループを即座に停止させます。

type ComplexMapper<T> = T extends string 
  ? string 
  : T extends object 
    ? { [K in keyof T]: ComplexMapper<any> } // コンパイラがこれ以上深く探索するのを停止させます
    : T;

修正の検証型を修正した後、以下の3つのステップでビルドの安定性を検証してください。

  • ターミナルでのチェック: npx tsc --noEmitを実行します。エラーなしでパスすれば、構造的な修正は確実です。- Intellisenseの監査: VS Codeで変数にホバーします。型が早々にany...と表示される場合、深度カウンターの設定が低すぎる可能性があります。- 型のテスト: tsdexpect-typeを使用して、型が依然として正しい形状に解決されるか確認します。``` import { expectType } from 'tsd';

// TCO版が配列を正しく処理することを確認する expectType<ReverseTCO<[1, 2, 3]>>([3, 2, 1]);


## 予防的なヒント生のAPIレスポンスのような、未知で巨大な構造に再帰的な型を適用することは避けてください。代わりに、明確な境界を定義しましょう。ライブラリを公開する場合は、常にユーティリティ型の「浅い(shallow)」代替手段を提供してください。これにより、巨大なデータ構造を持つユーザーが、あなたの再帰制限を継承してしまうのを防ぐことができます。

Related Error Notes