TypeScript で「Type instantiation is excessively deep and possibly infinite」エラーが出た時の対処法

TypeScript でジェネリクスやユーティリティ型を多用していると、まれに以下のようなエラーに遭遇します。

Type instantiation is excessively deep and possibly infinite.

これは「型の展開があまりにも深く、TypeScript コンパイラが無限再帰のような構造を検知した」という意味です。
再帰的な型定義や、ネストしたユーティリティ型(Pick, Omit, ReturnType など)を組み合わせると起きやすく、非常に理解しづらいエラーでもあります。

本記事ではこのエラーの原因、再現例、回避方法、そして実務での対処の考え方をまとめます。

1. エラーの再現例と発生パターン

まずは典型的な再帰型を使った例を見てみましょう。

type DeepReadonly<T> = {
  readonly [K in keyof T]: DeepReadonly<T[K]>;
};

interface User {
  name: string;
  friend?: User;
}

type ReadonlyUser = DeepReadonly<User>; // ❌ Error: Type instantiation is excessively deep and possibly infinite.

この例では、User の中に自分自身を含む再帰的な構造 (friend?: User) があるため、DeepReadonly 型を展開しようとすると、TypeScript は無限再帰のような構造を検出します。

TypeScript は安全のため、型展開の深さに上限(おおむね約50レベル)を設けており、それを超えるとこのエラーが発生します。

他の発生例

  • NestedPartial<T> のように部分型を再帰的に定義
  • JSON 型などの深い構造体 (Record<string, any>)
  • ReturnTypeParameters を多段階で組み合わせた型
  • Redux Toolkit や Zod, Prisma のようなライブラリを型でラップ

2. 根本原因:TypeScript の型展開制限

TypeScript の型システムは静的解析の段階で全ての型を評価します。
そのため、自己参照的な型や、入れ子になったジェネリクスが連鎖的に展開されると、最終的に「深すぎる型展開」が発生します。

TypeScript チームはこの無限展開を防ぐため、内部的に「型の再帰深度リミット」を設けています。
このリミットを超えると「possibly infinite(無限かもしれない)」と判断し、強制的に型評価を中止します。

3. 対策1:再帰を制限するガード条件を設ける

最も効果的な解決策は、「再帰の深さを制限する」ことです。
TypeScript 4.5以降では、条件付き型を用いて再帰の末端を明示的に止める書き方が可能です。

type DeepReadonly<T, Depth extends number = 10> = 
  Depth extends 0 ? T : 
  T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K], Decrement[Depth]> } : T;

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

Depth を設けることで、展開回数を強制的に制限し、コンパイラが深すぎる型を展開しないようにします。

4. 対策2:as const / any / unknown で評価を止める

再帰の深さが不可避なケースでは、TypeScript に「これ以上型推論しないでほしい」と伝えることが重要です。
as constas any, as unknown を適切に使うと、型展開が打ち切られ、エラーを回避できます。

const config = {
  theme: {
    colors: {
      primary: '#000',
      secondary: '#fff'
    }
  }
} as const; // ✅ 深い構造でも安全

あるいは、再帰関数の戻り値型に anyunknown を与えることで、型展開の深さを抑制できます。

function parseJson<T>(value: string): T | unknown {
  return JSON.parse(value)
}

このように、型推論を止める「境界」を作ることで、TypeScript の再帰展開を制御できます。

5. 対策3:ユーティリティ型の簡略化

Pick, Omit, Extract, Exclude, ReturnType などを多段に重ねているときも、エラーの原因になります。
この場合は、型エイリアスを途中に挟んで複雑さを分割するのが有効です。

type A = Pick<User, 'name' | 'friend'>;
type B = DeepReadonly<A>; // ✅ 一段階にするだけでエラー解消

あるいは、ユーティリティ型のチェーンを infer で展開しない形に変えると、TypeScript の評価が早く終わります。

type Simplify<T> = { [K in keyof T]: T[K] };
type SafeReadonly<T> = Simplify<DeepReadonly<T>>;

この Simplify トリックは、複雑な再帰的ジェネリクスを flatten(平坦化)してくれるため、型のネストを減らす効果があります。

6. 実務での回避戦略と考え方

このエラーは開発者の過失というより、TypeScript の仕様的な限界に近いものです。
ライブラリ開発や大規模アプリでは、以下のような戦略を取ると現実的です。

  • 1. 再帰型を制御する
    深さを明示的に制限するか、末端条件を定義する
  • 2. 型を一度 Flatten する
    中間層の型を type Alias = ReturnType<typeof fn> のように分ける
  • 3. 複雑なジェネリクスを避ける
    どうしても複雑になる場合は、as constas any を活用
  • 4. 型ツールを使う
    ts-toolbelttype-fest は最適化済みの型ユーティリティを提供しており、自作するより安全

簡略化の例

import type { Simplify } from 'type-fest'

type MyType = Simplify<DeepReadonly<User>>

まとめ

「Type instantiation is excessively deep and possibly infinite」エラーは、TypeScript の型システムが自己参照的な構造を安全に評価できないときに発生します。
本質的な原因は「型展開が深すぎる」ことにあるため、再帰型に制限をかけたり、as constSimplify を使って評価を止めたりするのが効果的です。
大規模プロジェクトでは、複雑なジェネリクスを小さく分割し、型推論を抑制することがメンテナンス性の向上にもつながります。

参考

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