TypeScriptプロジェクトは成長します。ファイルは増え、依存関係は絡み合います。そして最終的に、ビルドはロジックの複雑さとは無関係な壁に突き当たります。それは、コンパイラが単一の宣言ファイルを書き出すために、宇宙全体を読み込む必要があるという問題です。

TypeScript 6.0は、isolatedDeclarationsによってこれに対処します。この機能は、.d.tsファイルがどのように生成されるかを再考するものです。宣言の生成をフルタイプのチェック・パイプラインに紐付けるのではなく、各ソースファイルを個別に見て、コンパイラがそれらのファイルを生成できるようにします。その結果、依存関係のグラフを一つずつ辿るのではなく、数千のファイルにわたって並列に実行できるビルドプロセスが実現します。

真のボトルネック

現在、宣言ファイルの生成はシリアル(逐次的)な操作です。--declarationを有効にしてコンパイラを実行すると、TypeScriptは、そのモジュールが触れるすべての型を完全に理解するまで、特定のモジュールの.d.tsファイルを生成できません。もしutils.tstypes.tsから型をインポートし、types.tsapi.tsから何かを取り込んでいる場合、コンパイラはutils.tsが何をエクスポートしているかを記述する前に、その連鎖を解決しなければなりません。

大規模なモノレポでは、この連鎖は過酷です。インポートグラフのルート付近にある単一のファイルが、数百のダウンストリームファイルの宣言生成をブロックすることがあります。CPUには8つのコアがありますが、TypeScriptがパッケージ境界を越えてあらゆるインターフェースの形状を苦労して再構築している間、そのうちの7つはアイドル状態のままです。コンパイラは必要な作業を行っていますが、型チェックと宣言生成の結合により、公開されている型だけをディスクに書き込みたい場合でも、ファイル間分析の全コストを支払うことになります。

isolatedDeclarationsがルールをどう変えるか

isolatedDeclarationsはその結合を断ち切ります。このフラグが有効になると、コンパイラは他のファイルに内容を尋ねることなく、ソースファイルに対して.d.tsファイルを生成することを前提とします。これを実現するために、シンプルな契約を要求します。すなわち、エクスポートされるすべてのシンボルは、それが宣言されている場所で、明示的かつ可視的な型注釈を持っている必要があります。

コンパイラがソース内に直接書かれた完全な型を見ることができれば、型推論を行う必要はありません。インポートを追いかける必要もありません。別のファイルにある識別子Userがインターフェースなのか、型エイリアスなのか、あるいはクラスなのかを知る必要もありません。単に、あなたが書いたものをそのまま出力するだけです。

つまり、ファイルAとファイルBは宣言を同時に生成できます。ビルドオーケストレーターは、各ファイルを個別のスレッドに渡すことができます。以前は完全な型チェッカーがなかったために.d.tsの生成をスキップしていた高速なトランスパイラも、作業が純粋に構文的なものになるため、宣言ファイルを生成できるようになります。

トレードオフ:明示的に記述する

スピードはタダでは手に入りません。エクスポートするものに対して、型推論に頼るのをやめなければなりません。すべての公開関数、クラス、変数、定数には、その型を明示的に記述する必要があります。TypeScriptが戻り文を調べたり、ジェネリック引数を解決したりして型を計算しなければならない場合、isolatedDeclarationsはエラーになります。

実践的な例を見てみましょう。フラグがない場合、次のように書くかもしれません:

export function fetchUser(id: number) {
  return fetch(`/users/${id}`).then(r => r.json());
}

TypeScriptは、fetch、次にPromise.prototype.then、そしてr.json()を返す匿名関数を検査することで、戻り値を推論します。.d.tsを生成するために、コンパイラはこれらすべての分析を実行する必要があります。

isolatedDeclarationsが有効な場合、エクスポートに注釈を付ける必要があります:

interface User {
  id: number;
  email: string;
}

export function fetchUser(id: number): Promise<User> {
  return fetch(`/users/${id}`).then(r => r.json());
}

これで、コンパイラはすぐにPromise<User>を確認できます。宣言を生成して次に進みます。

このルールは広く適用されます。エクスポートされる配列は、要素から推論させるのではなく、明示的な型が必要です。エクスポートされるオブジェクトは、その形状がコンシューマーにとって重要な場合、明示的な型注釈が必要です。ジェネリック関数は、宣言場所で戻り値の型と制約が可視化されている必要があります。複雑なマップ型(mapped type)の結果を、完全に書き出された名前付きの型エイリアスを与えずにエクスポートすることはできません。

メリットは、公開APIが自己文書化されることです。コンシューマー(およびコンパイラ)は、実装の詳細から意図を読み解く必要がなくなります。型は意図的な契約となります。

ビルド時間の短縮

大規模なコードベースでは、その影響は即座に現れます。宣言の生成が最大のボトルネックでなくなるため、数分かかっていたビルド時間が数秒に短縮される可能性があります。各ファイルが独立して生成されるため、プロセスはインポートグラフの深さではなく、利用可能なコア数に応じてスケールします。

This also changes what tools you can use. Transpilers like esbuild and swc are already lightning-fast at turning TypeScript into JavaScript, but many teams still run tsc separately just to produce .d.ts files. With isolatedDeclarations, those fast tools can handle both jobs. They do not need to replicate TypeScript's entire type system to generate declarations; they only need to parse syntax and copy the explicit types you provided. That makes end-to-end TypeScript builds with alternative toolchains far more viable.

Distributed and incremental builds get simpler too. In continuous integration, a remote cache or a sharded build can emit declarations for a package without downloading its full transitive dependency graph first. If the types are explicit in the source, the build shard has everything it needs.

変わらないこと

この制約はエクスポートに対してのみ適用されます。モジュール内部では、これまで通り動作します。ローカル変数、プライベートなクラスメンバ、およびエクスポートされていないヘルパー関数は、引き続き完全な型推論に頼ることができます。TypeScriptは、ループ変数やクロージャのパラメータの型を、不満を漏らすことなく快く推論してくれます。

export function calculateTotal(items: Item[]): number {
  // Local variable: inference is fine
  const taxRate = 0.08;
  
  // Private class member inside a local class: inference is fine
  class Helper {
    private cache = new Map();
  }
  
  return items.reduce((sum, item) => sum + item.price * (1 + taxRate), 0);
}

注釈が必要なのは、エクスポートされた関数のシグネチャだけです。内部の仕組みは、自由で表現力豊かなまま保たれます。これにより、開発時の負担は許容できる範囲に収まります。あらゆるところを完全に明示的なスタイルに切り替えるのではなく、単に各モジュールの境界における「契約」を形式化するだけなのです。

あなたのコードベースに適しているか?

isolatedDeclarationsを採用すると、時間の使い方が変わります。エクスポートを書く際に数回のキー入力が増えますが、その引き換えに、あらゆるビルドで支払っていた「利息」を払わずに済みます。ライブラリの開発者にとって、これは納得しやすい提案でしょう。パブリックAPIには、そもそも注釈を付けるべきだからです。クローズドなモノレポ内で作業するアプリケーション開発者にとっては、事前のコストが不必要な儀式のように感じられるかもしれません。しかし、もしチームがビルド時間を「コーヒー休憩の時間」で計測しているような状況であれば、このトレードオフはすぐに魅力的なものとなります。

段階的に導入することも可能です。フラグを有効にし、コンパイラを実行して、エクスポートされたシンボルに対して発生したエラーを修正していきます。エラーメッセージには、どのパブリックな型が暗黙的であるかが正確に示されます。それらを修正し、内部実装には手を触れなければ、宣言ファイルの生成ステップが加速していくのを実感できるはずです。

一つ覚えておくべきことがあります。このフラグは、TypeScriptの型チェッカー自体の速度を上げるものではありません。エディタでのフィードバックを速めたい、あるいはtsc --noEmitの実行を速めたい場合は、引き続きプロジェクト参照(project references)や、より厳格なファイル包含設定、あるいはその他のアーキテクチャ的な修正が必要になります。isolatedDeclarationsは、あくまで.d.tsファイルの生成をターゲットにしたものです。これはビルドの最適化であり、型チェックの最適化ではありません。

真の要点

isolatedDeclarationsは、パブリックな型を「第一級の成果物」として扱うことを求めています。コンパイラに推論させるのをやめ、自分で書き記しましょう。一度そうすれば、コンパイラは宣言ファイルを生成するたびに依存関係グラフ全体を探索し続ける必要がなくなります。並列での生成が可能になり、esbuildswcのようなツールで完全なTypeScriptワークフローを扱えるようになり、モノレポのビルドが停滞することもなくなります。

コストは「ビルド時間」から「記述時間」へと移行します。成長を続けるほとんどのチームにとって、それは価値のあるトレードオフです。