はじめに
TypeScriptのコードベースが成長するにつれ、単一のtsconfig.jsonによる管理は限界を迎えます。tscを呼び出すたびにプロジェクト全体の型チェックと出力が実行され、大規模モノレポでは--noEmitでも数分かかることがあります。さらに、モジュール境界がないため任意のファイルが他のファイルをインポートでき、複雑な依存グラフが生まれてリファクタリングが困難になります。TypeScript Project Referencesは、コードベースを独立したサブプロジェクトに分割し、それぞれに個別のtsconfig.jsonを持たせることで、インクリメンタルビルド、強制されたAPI境界、劇的に高速な型チェックを実現します。
Project Referencesとは
Project Referencesは、TypeScriptプロジェクトが別のTypeScriptプロジェクトに依存することを宣言する仕組みです。各サブプロジェクトはtsconfig.jsonでcomposite: trueを設定し、利用側のプロジェクトはreferences配列で依存関係を宣言します。
{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"rootDir": "src",
"outDir": "dist"
}
}
{
"references": [
{ "path": "../packages/core" },
{ "path": "../packages/utils" }
]
}
ビルド時、TypeScriptは参照されたプロジェクトをトポロジカル順に先にビルドし、.d.tsと.jsの出力を生成します。これにより、明確で非循環の依存グラフが強制されます。プロジェクトAがプロジェクトBを参照している場合、BがAを参照することはできません。循環依存を試みるとコンパイルエラーが発生します。
Compositeプロジェクトとビルドモード
Compositeプロジェクト(composite: true)は、他のプロジェクトから参照されることを意図したプロジェクトです。declaration: true、rootDir、outDirが必須です。declarationMap: trueを有効にすると、.d.ts.mapファイルが生成され、エディタの「定義へ移動」機能が.d.tsではなく元の.tsソースに直接ジャンプするようになります。
ビルドモードのコマンドtsc --build(またはtsc -b)は、プロジェクトグラフ全体をインテリジェントに処理します。
# すべてのプロジェクトと依存関係をビルド
tsc -b
# すべてのプロジェクトを強制リビルド
tsc -b --force
# ビルド出力をクリーン
tsc -b --clean
# ドライラン — ビルド予定を表示
tsc -b --dry
ビルドモードは.tsbuildinfoファイルを使用してファイルハッシュと依存関係のメタデータを追跡します。変更がないプロジェクトはスキップされるため、大規模モノレポではビルド時間が50〜90%削減されます。incremental: trueはcompositeによって暗黙的に有効になりますが、tsBuildInfoFileオプションでビルド情報ファイルの出力先を制御できます。
モノレポ統合
Project Referencesはモノレポツールと自然に統合できます。典型的な構造は以下の通りです。
packages/
core/ tsconfig.json (composite: true)
utils/ tsconfig.json (composite: true)
apps/
web/ tsconfig.json (references: [core, utils])
api/ tsconfig.json (references: [core, utils])
tsconfig.base.json
各種モノレポツールのサポート状況は次の通りです。
| ツール | サポート |
|---|---|
| npm/pnpm/yarn workspaces | 良好に連携。ルートtsconfig.jsonでreferencesを宣言 |
| Turborepo | inputsとdependsOnでネイティブサポート |
| Nx | Project Referencesを自動検出しビルドを並列化 |
| Lerna | 手動設定が必要 |
宣言マップと開発者体験
最も影響が大きく、かつ過小評価されている機能の一つが**宣言マップ(declaration maps)**です。利用側プロジェクトで型定義に移動しようとしたとき、宣言マップがないと.d.tsファイルにしかジャンプできません。declarationMap: trueを有効にすると、元の.tsソースに直接ジャンプできるようになります。
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}
宣言マップはランタイムオーバーヘッドがまったくなく、エディタツールのみで使用されます。sourceMap: trueと組み合わせることで、開発から本番まで一貫したデバッグ体験が得られます。
CI最適化
Project Referencesの真価はCIで発揮されます。.tsbuildinfoファイルを実行間でキャッシュすることで(@actions/cacheなどを使用)、パイプライン実行をまたいでインクリメンタルビルドの情報を保持できます。
# GitHub Actions — .tsbuildinfoファイルをキャッシュ
- uses: actions/cache@v3
with:
path: "**/*.tsbuildinfo"
key: tsbuildinfo-${{ runner.os }}-${{ hashFiles('**/tsconfig*.json') }}
選択的型チェックにはtsc -b --noEmitを使用します。これによりコードベース全体ではなく、前回のビルドから変更があったプロジェクトのみが型チェックされます。--forceはメインブランチのビルドのみに使用し、PRのCIではインクリメンタルモードに依存しましょう。超大規模モノレポではnx affectedなどのツールを使って、変更の影響を受けるプロジェクトのみにスコープを絞ることも可能です。
まとめ
TypeScript Project Referencesは大規模TypeScriptコードベースのスケーリング問題を解決します。まずはコアとなる型パッケージ、ユーティリティパッケージ、アプリケーションコードの2〜3の論理パッケージに分割することから始めましょう。宣言マップ付きのcomposite: trueで開発者体験を高め、tsc -bで高速なインクリメンタルビルドを活用し、CIでは.tsbuildinfoファイルをキャッシュしてその恩恵をパイプライン実行全体に拡張します。TurborepoやNxなどのツールはProject Referencesをキャッシュと並列実行でさらに強化し、モノレポ環境での力を最大限に引き出します。
