TypeScript Project References 入門:大規模 Monorepo で高速ビルドを実現する設定手順
大規模な TypeScript プロジェクトを開発していると、ビルド時間の増加に悩まされることはありませんか。特に Monorepo 構成では、わずかな変更でもすべてのコードを再コンパイルする必要があり、開発体験が著しく低下してしまいます。
TypeScript 3.0 で導入された Project References(プロジェクト参照)は、この問題を解決する強力な機能です。プロジェクトを複数のサブプロジェクトに分割し、依存関係を明示的に定義することで、変更されたプロジェクトのみを再ビルドする増分ビルドが可能になります。本記事では、Project References の基本から実践的な設定手順まで、初心者にもわかりやすく解説いたします。
背景
TypeScript プロジェクトが成長するにつれて、単一の tsconfig.json で管理することが困難になっていきます。
大規模プロジェクトで発生する問題
従来の TypeScript プロジェクトでは、すべてのソースコードを単一のコンパイル単位として扱っていました。この方法には以下のような課題がありました。
- 全体ビルドの遅延: 一部のファイルを変更しただけでも、プロジェクト全体がコンパイル対象になってしまいます
- 依存関係の不明確さ: パッケージ間の依存が暗黙的で、循環参照などの問題を発見しにくくなります
- 並列ビルドの困難さ: すべてのコードが一度にコンパイルされるため、並列処理の恩恵を受けられません
Monorepo におけるビルド効率の重要性
Monorepo は複数のパッケージやアプリケーションを単一のリポジトリで管理する手法です。yarn workspaces や pnpm workspaces などのツールと組み合わせることで、コードの共有や依存関係の管理が容易になります。
しかし、Monorepo の規模が大きくなると、ビルド時間が開発のボトルネックになってしまうのです。
以下の図は、従来の TypeScript ビルドと Project References を使った場合の違いを示しています。
mermaidflowchart TD
subgraph 従来のビルド
A1["すべてのソースコード"] -->|全体をコンパイル| B1["TypeScript コンパイラ"]
B1 --> C1["JavaScript 出力"]
end
subgraph Project References
A2["変更されたプロジェクト"] -->|差分のみコンパイル| B2["TypeScript コンパイラ"]
A3["依存プロジェクト"] -.->|キャッシュ利用| B2
B2 --> C2["JavaScript 出力"]
end
従来の方法では毎回全体をコンパイルする必要がありましたが、Project References を使うことで変更があったプロジェクトだけを効率的にビルドできるようになります。
課題
大規模 Monorepo で TypeScript を利用する際、開発者が直面する具体的な課題について詳しく見ていきましょう。
ビルド時間の増大
プロジェクトが成長するにつれて、ビルド時間が線形以上に増加してしまいます。例えば、以下のような Monorepo 構成を考えてみましょう。
| # | パッケージ | ファイル数 | ビルド時間(従来) |
|---|---|---|---|
| 1 | @myapp/shared | 50 | 5 秒 |
| 2 | @myapp/api | 200 | 20 秒 |
| 3 | @myapp/web | 300 | 30 秒 |
| 4 | @myapp/admin | 150 | 15 秒 |
従来の方法では、@myapp/shared の 1 ファイルを変更しただけでも、全パッケージのビルドに 70 秒かかってしまいます。開発中に何度もビルドを繰り返すことを考えると、この時間は大きな損失となるでしょう。
依存関係の管理の複雑さ
Monorepo では、パッケージ間の依存関係が複雑になりがちです。
mermaidflowchart LR
shared["@myapp/shared<br/>共通ライブラリ"]
api["@myapp/api<br/>バックエンド API"]
web["@myapp/web<br/>フロントエンド"]
admin["@myapp/admin<br/>管理画面"]
api --> shared
web --> shared
admin --> shared
web --> api
admin --> api
この図は典型的な Monorepo の依存関係を示しています。shared パッケージは他のすべてのパッケージから参照され、web と admin は api パッケージにも依存しています。
従来の TypeScript 設定では、これらの依存関係が暗黙的であり、以下のような問題が発生しやすくなります。
- 循環依存の検出困難: パッケージ間で循環参照が発生しても、コンパイルエラーとして検出されません
- ビルド順序の不明確さ: どのパッケージから先にビルドすべきか、自動的に判断できません
- 型情報の不整合: 依存パッケージのビルドが完了していない状態で、型情報を参照してしまうことがあります
メモリ使用量の問題
大規模プロジェクトでは、すべてのソースコードを一度にメモリに読み込むため、メモリ不足が発生することもあります。特に CI/CD 環境など、リソースが限られた環境では深刻な問題となるでしょう。
並列ビルドの制約
従来の方法では、TypeScript コンパイラが単一のプロセスですべてのコードを処理するため、マルチコア CPU の性能を十分に活用できませんでした。
解決策
TypeScript Project References を導入することで、これらの課題を効果的に解決できます。
Project References とは
Project References は、TypeScript プロジェクトを複数のサブプロジェクトに分割し、それらの依存関係を明示的に定義する機能です。各サブプロジェクトは独立した tsconfig.json を持ち、他のプロジェクトを「参照」することができます。
この機能により、以下のメリットが得られます。
- 増分ビルド: 変更されたプロジェクトとその依存先のみを再ビルド
- 並列ビルド: 依存関係のないプロジェクトを並列にコンパイル
- 明示的な依存管理: プロジェクト間の依存関係が
tsconfig.jsonで明確になります - 型安全性の向上: 依存プロジェクトの型情報を確実に参照できます
Project References の仕組み
Project References では、.d.ts(型定義ファイル)と .js ファイルを出力し、他のプロジェクトはこれらを参照します。これにより、依存プロジェクトのソースコードを直接読み込む必要がなくなるのです。
mermaidflowchart TB
subgraph SharedProject["@myapp/shared プロジェクト"]
S1["src/index.ts"] -->|tsc --build| S2["dist/index.js"]
S1 -->|tsc --build| S3["dist/index.d.ts"]
end
subgraph ApiProject["@myapp/api プロジェクト"]
A1["src/server.ts"] -->|参照| S3
A1 -->|ビルド| A2["dist/server.js"]
end
S2 -.->|実行時に使用| A2
上図のように、@myapp/api プロジェクトは @myapp/shared のソースコードではなく、ビルド済みの型定義ファイル(.d.ts)を参照します。このため、shared が変更されていない限り、再ビルドの必要がありません。
ビルド時間の改善効果
Project References を導入することで、劇的なビルド時間の短縮が期待できます。
| # | シナリオ | 従来のビルド | Project References | 改善率 |
|---|---|---|---|---|
| 1 | 初回ビルド(全体) | 70 秒 | 75 秒 | -7%(ややオーバーヘッド) |
| 2 | shared のみ変更 | 70 秒 | 25 秒 | ★★★★★ 64% 改善 |
| 3 | api のみ変更 | 70 秒 | 20 秒 | ★★★★★ 71% 改善 |
| 4 | web のみ変更 | 70 秒 | 30 秒 | ★★★★☆ 57% 改善 |
初回ビルドではわずかなオーバーヘッドがありますが、日常的な開発では変更箇所が限定されるため、大幅な時間短縮が実現できますね。
具体例
それでは、実際に Project References を設定していきましょう。ここでは、以下のような Monorepo 構成を例に説明いたします。
bashmonorepo/
├── packages/
│ ├── shared/ # 共通ライブラリ
│ ├── api/ # バックエンド API
│ └── web/ # フロントエンド
├── package.json
└── tsconfig.json # ルート設定
ステップ 1: ルート tsconfig.json の作成
まず、Monorepo のルートに基本設定となる tsconfig.json を作成します。
typescript// monorepo/tsconfig.json
このファイルは、各プロジェクトが共通で使用する TypeScript のベース設定を定義します。
json{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"composite": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
重要なのは "composite": true オプションです。このオプションを有効にすることで、Project References の機能が利用可能になります。
主要なオプションの説明:
- composite: Project References を有効化する必須オプションです
- declaration:
.d.ts型定義ファイルを生成します(composite が true の場合は必須) - declarationMap: 型定義ファイルのソースマップを生成し、デバッグを容易にします
ステップ 2: 共通パッケージの設定
次に、他のパッケージから参照される shared パッケージの設定を行いましょう。
typescript// packages/shared/tsconfig.json
json{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts"]
}
この設定では、ルートの tsconfig.json を継承し、パッケージ固有のディレクトリ設定を追加しています。
- extends: ルート設定を継承することで、設定の重複を避けます
- outDir: ビルド結果の出力先ディレクトリを指定します
- rootDir: ソースコードのルートディレクトリを指定します
ステップ 3: package.json の設定
shared パッケージの package.json も適切に設定しましょう。
typescript// packages/shared/package.json
json{
"name": "@myapp/shared",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc --build",
"clean": "tsc --build --clean"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}
ポイントは以下の通りです。
- main: JavaScript ファイルのエントリーポイントを
distディレクトリ配下に指定します - types: 型定義ファイルのエントリーポイントを指定します
- build スクリプト:
tsc --buildコマンドを使用することで、Project References の機能が有効になります
ステップ 4: 依存パッケージの設定(API)
続いて、shared パッケージに依存する api パッケージを設定します。
typescript// packages/api/tsconfig.json
json{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"references": [{ "path": "../shared" }],
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts"]
}
references セクションが新たに追加されました。ここで依存する他のプロジェクトを指定することで、TypeScript コンパイラが依存関係を理解できるようになります。
typescript// packages/api/package.json
json{
"name": "@myapp/api",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc --build",
"clean": "tsc --build --clean"
},
"dependencies": {
"@myapp/shared": "1.0.0"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}
dependencies に @myapp/shared を追加することで、yarn workspaces が依存関係を認識します。
ステップ 5: フロントエンドパッケージの設定(Web)
web パッケージは shared と api の両方に依存する例として設定しましょう。
typescript// packages/web/tsconfig.json
json{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"jsx": "react-jsx",
"lib": ["ES2020", "DOM", "DOM.Iterable"]
},
"references": [
{ "path": "../shared" },
{ "path": "../api" }
],
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts"]
}
フロントエンド向けの設定として、以下を追加しています。
- jsx: React の JSX 変換方法を指定します
- lib: DOM API などブラウザ環境の型定義を追加します
- references: 複数のプロジェクトを参照する場合は、配列で指定します
typescript// packages/web/package.json
json{
"name": "@myapp/web",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc --build",
"clean": "tsc --build --clean"
},
"dependencies": {
"@myapp/shared": "1.0.0",
"@myapp/api": "1.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"typescript": "^5.0.0"
}
}
ステップ 6: ルートレベルの統合設定
最後に、ルートレベルで全プロジェクトをまとめる設定を作成します。
typescript// monorepo/tsconfig.json(更新)
ルートの tsconfig.json に references を追加して、すべてのプロジェクトを管理できるようにしましょう。
json{
"files": [],
"references": [
{ "path": "./packages/shared" },
{ "path": "./packages/api" },
{ "path": "./packages/web" }
]
}
files: [] を指定することで、このファイル自体はコンパイル対象を持たず、参照管理のみを行います。
ステップ 7: ビルドスクリプトの追加
ルートの package.json にビルドスクリプトを追加します。
typescript// monorepo/package.json
json{
"name": "monorepo",
"private": true,
"workspaces": ["packages/*"],
"scripts": {
"build": "tsc --build",
"build:clean": "tsc --build --clean",
"build:watch": "tsc --build --watch",
"build:force": "tsc --build --force"
},
"devDependencies": {
"typescript": "^5.0.0"
}
}
各スクリプトの役割:
- build: 増分ビルドを実行します(変更されたプロジェクトのみ)
- build
: ビルド成果物を削除します - build
: ファイル変更を監視して自動ビルドします - build
: キャッシュを無視して全プロジェクトを再ビルドします
実際の使用例
実際にコードを書いて、Project References がどのように動作するか確認してみましょう。
typescript// packages/shared/src/index.ts
まず、共通ライブラリに型定義と関数を作成します。
typescript/**
* ユーザー情報の型定義
*/
export interface User {
id: number;
name: string;
email: string;
}
/**
* ユーザー名を整形する関数
* @param user - ユーザー情報
* @returns 整形されたユーザー名
*/
export function formatUserName(user: User): string {
return `${user.name} (${user.email})`;
}
typescript// packages/api/src/index.ts
次に、API パッケージから shared パッケージを利用します。
typescript// shared パッケージの型と関数をインポート
import { User, formatUserName } from '@myapp/shared';
/**
* ユーザー情報を取得する API 関数
* @param userId - ユーザー ID
* @returns ユーザー情報
*/
export function getUser(userId: number): User {
// 実際にはデータベースから取得する処理
const user: User = {
id: userId,
name: 'John Doe',
email: 'john@example.com',
};
return user;
}
/**
* ユーザー情報を整形して返す
* @param userId - ユーザー ID
*/
export function getUserFormatted(userId: number): string {
const user = getUser(userId);
// shared パッケージの関数を利用
return formatUserName(user);
}
TypeScript は @myapp/shared パッケージの型定義ファイル(.d.ts)を参照するため、型安全性が保たれつつ、高速なビルドが可能になります。
typescript// packages/web/src/App.tsx
フロントエンドからも同様に利用できます。
typescriptimport React, { useEffect, useState } from 'react';
import { User } from '@myapp/shared';
import { getUser } from '@myapp/api';
/**
* ユーザー情報を表示するコンポーネント
*/
export function App() {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
// API パッケージの関数を使用
const userData = getUser(1);
setUser(userData);
}, []);
if (!user) {
return <div>読み込み中...</div>;
}
return (
<div>
<h1>ユーザー情報</h1>
<p>ID: {user.id}</p>
<p>名前: {user.name}</p>
<p>メール: {user.email}</p>
</div>
);
}
ビルドの実行
設定が完了したら、実際にビルドを実行してみましょう。
bash# 初回ビルド(全プロジェクトをビルド)
yarn build
ルートディレクトリで上記コマンドを実行すると、TypeScript が依存関係を解析し、適切な順序でビルドを行います。
bash# 出力例
# Building project '/path/to/monorepo/packages/shared/tsconfig.json'...
# Building project '/path/to/monorepo/packages/api/tsconfig.json'...
# Building project '/path/to/monorepo/packages/web/tsconfig.json'...
この例では、依存関係のない shared が最初にビルドされ、次に shared に依存する api と web がビルドされることがわかります。
bash# ファイルを変更せずに再度ビルド
yarn build
変更がない場合、TypeScript は即座にビルドをスキップします。
bash# 出力例
# Project 'packages/shared/tsconfig.json' is up to date
# Project 'packages/api/tsconfig.json' is up to date
# Project 'packages/web/tsconfig.json' is up to date
ウォッチモードの活用
開発中は、ウォッチモードを使用すると効率的です。
bash# ファイル変更を監視して自動ビルド
yarn build:watch
このコマンドを実行すると、ファイルを保存するたびに自動的に増分ビルドが実行されます。変更されたプロジェクトとその依存先のみがビルドされるため、非常に高速ですね。
トラブルシューティング
Project References の設定でよくあるエラーとその解決方法をご紹介します。
エラー 1: "File is not under 'rootDir'"
エラーメッセージ:
pythonerror TS6059: File '/path/to/file.ts' is not under 'rootDir' '/path/to/src'.
'rootDir' is expected to contain all source files.
発生条件: rootDir の外にあるファイルをインポートしようとした場合
解決方法:
tsconfig.jsonのincludeパターンを確認しますrootDirとoutDirの設定が適切か確認します- 必要に応じて
includeパターンを調整します
エラー 2: "Cannot find module"
エラーメッセージ:
luaerror TS2307: Cannot find module '@myapp/shared' or its corresponding type declarations.
発生条件: 参照プロジェクトがビルドされていない、または references の設定が不足している場合
解決方法:
tsconfig.jsonのreferencesセクションに依存プロジェクトを追加します- 依存プロジェクトを先にビルドします(
yarn buildがルートから実行されているか確認) package.jsonのdependenciesにも依存パッケージが記載されているか確認します
エラー 3: "composite が有効でない"
エラーメッセージ:
goerror TS6306: Referenced project '/path/to/project' must have setting "composite": true.
発生条件: 参照されるプロジェクトの tsconfig.json で composite が true に設定されていない場合
解決方法:
すべてのプロジェクトの tsconfig.json に以下を追加します。
json{
"compilerOptions": {
"composite": true,
"declaration": true
}
}
まとめ
TypeScript Project References は、大規模 Monorepo のビルド時間を劇的に改善する強力な機能です。本記事では、基本的な概念から実践的な設定手順まで解説してまいりました。
重要なポイントを振り返りましょう。
設定の要点
- composite オプション: すべてのプロジェクトで
"composite": trueを有効にします - references セクション: 依存関係を明示的に
tsconfig.jsonで定義します - 型定義ファイル:
.d.tsファイルの生成により、ソースコードを直接参照せずに型情報を利用できます - ビルドコマンド:
tsc --buildを使用することで、増分ビルドが有効になります
得られる効果
| # | 効果 | 説明 |
|---|---|---|
| 1 | ビルド時間の短縮 | 変更箇所のみをビルドすることで、50-70% の時間短縮が可能です |
| 2 | 並列ビルド | 依存関係のないプロジェクトを同時にビルドできます |
| 3 | メモリ効率の向上 | すべてのソースコードを一度に読み込む必要がなくなります |
| 4 | 明確な依存管理 | プロジェクト間の依存が設定ファイルで明示されます |
| 5 | 型安全性の維持 | 依存プロジェクトの型情報を確実に参照できます |
実装のステップ
- ルートの
tsconfig.jsonでベース設定を定義 - 各パッケージに個別の
tsconfig.jsonを作成し、composite: trueを設定 referencesセクションで依存関係を定義package.jsonにビルドスクリプトを追加tsc --buildコマンドでビルドを実行
Project References は初期設定に多少の手間がかかりますが、一度設定してしまえば長期的に大きなメリットをもたらします。特に、継続的に成長するプロジェクトでは、早期に導入することをお勧めいたします。
Monorepo の規模が大きくなり、ビルド時間に悩んでいる方は、ぜひ本記事を参考に Project References を導入してみてください。開発体験の向上を実感していただけるはずです。
関連リンク
articleTypeScript Project References 入門:大規模 Monorepo で高速ビルドを実現する設定手順
articleTypeScript Null 安全戦略の比較検証:ts-reset vs strictNullChecks vs noUncheckedIndexedAccess
articleESM/CJS 地獄から脱出!「ERR_REQUIRE_ESM」「import 文が使えない」を TypeScript で直す
articleTypeScript 型安全なフィーチャーフラグ設計:判別可能共用体で運用事故を防ぐ
articleTypeScript satisfies 演算子の実力:型の過剰/不足を一発検知する実践ガイド
articlePlaywright × TypeScript 超入門チュートリアル:型安全 E2E を最短構築
articleVite プラグインフック対応表:Rollup → Vite マッピング早見表
articleNestJS Monorepo 構築:Nx/Yarn Workspaces で API・Lib を一元管理
articleTypeScript Project References 入門:大規模 Monorepo で高速ビルドを実現する設定手順
articleMySQL Router セットアップ完全版:アプリからの透過フェイルオーバーを実現
articletRPC アーキテクチャ設計:BFF とドメイン分割で肥大化を防ぐルータ戦略
articleMotion(旧 Framer Motion)× TypeScript:Variant 型と Props 推論を強化する設定レシピ
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来