TypeScript Project Referencesのセットアップ手順 大規模Monorepoで高速ビルドするtsconfig設定
大規模なMonorepoでTypeScriptのビルド時間に悩まされたことはありませんか。私が実際に担当していた業務プロジェクトでは、5つのパッケージで構成されるMonorepo環境において、単一ファイルの修正でもフルビルドに3分以上かかるという深刻な問題を抱えていました。
開発者がコードを修正するたびに3分待つ状況は、生産性の大幅な低下を意味します。この課題に対し、私はTypeScript Project Referencesを導入することで、ビルド時間を平均30秒程度まで短縮することに成功しました。
この記事では、実際の業務プロジェクトでProject Referencesを導入した経験をもとに、セットアップ手順から運用時の注意点、実際に遭遇したエラーと解決方法まで、実践的な内容をお伝えします。
想定読者と記事の目的
この記事は、以下のような状況の方に役立つ内容となっています。
- Monorepo構成のTypeScriptプロジェクトでビルド時間に悩んでいる方
- 複数パッケージの依存関係管理に課題を感じている方
- CI/CDパイプラインのビルド時間を短縮したい方
- Project Referencesの具体的なセットアップ手順を知りたい方
検証環境
本記事では、以下の環境で動作確認を行っています。
- OS: macOS Sequoia 15.2
- Node.js: 22.12.0 LTS
- 主要パッケージ:
- TypeScript: 5.7.2
- Yarn: 4.5.3
- React: 18.3.1
- Next.js: 15.1.0
- 検証日: 2025年12月24日
背景
実務で遭遇したビルド時間の問題
私が担当していたプロジェクトは、以下のような構成のMonorepoでした。
mermaidflowchart LR
shared["@myapp/shared<br/>共通ライブラリ<br/>型定義・ユーティリティ"]
api["@myapp/api<br/>バックエンドAPI<br/>Express + NestJS"]
web["@myapp/web<br/>顧客向けサイト<br/>Next.js"]
admin["@myapp/admin<br/>管理画面<br/>React SPA"]
mobile["@myapp/mobile<br/>モバイルAPI<br/>React Native用"]
api --> shared
web --> shared
admin --> shared
mobile --> shared
web -.-> api
admin -.-> api
各パッケージの規模は次の通りでした。
| # | パッケージ | TypeScriptファイル数 | 主な用途 |
|---|---|---|---|
| 1 | @myapp/shared | 約120ファイル | 型定義、バリデーション関数 |
| 2 | @myapp/api | 約350ファイル | RESTful API、認証処理 |
| 3 | @myapp/web | 約500ファイル | 顧客向けWebアプリケーション |
| 4 | @myapp/admin | 約280ファイル | 社内向け管理画面 |
| 5 | @myapp/mobile | 約200ファイル | モバイルアプリ用API |
当初は単一のtsconfig.jsonですべてを管理していたため、たとえば@myapp/shared内の1つの型定義を修正しただけでも、全パッケージ(合計約1450ファイル)が再コンパイルの対象となっていました。
開発フローへの深刻な影響
この状況は、以下のような問題を引き起こしていました。
開発中のフィードバックループが極端に遅くなり、コードを修正してから動作確認できるまでに3分以上待つ必要がありました。1日に20回程度のビルドを行うと仮定すると、1日あたり約1時間がビルド待ち時間として失われる計算になります。
さらに、CI/CDパイプラインでも同じ問題が発生していました。GitHub Actionsで実行するビルドジョブが毎回フルビルドとなり、プルリクエストごとに5〜6分かかっていたのです。チームメンバーが1日10件のプルリクエストを作成すると、CIの実行時間だけで合計50〜60分になり、GitHub Actionsの無料枠を圧迫する要因にもなっていました。
TypeScriptの静的型付けとビルドパフォーマンスのジレンマ
TypeScriptの静的型付けは、コードの品質と保守性を高める重要な機能です。しかし、大規模プロジェクトでは型チェックとコンパイルのコストが無視できなくなります。
私たちのチームでも「ビルドが遅いならJavaScriptに戻すべきでは?」という議論が出たこともありました。しかし、型安全性を失うことは長期的な保守コストの増加につながるため、TypeScriptを維持しつつビルド時間を改善する方法を模索する必要がありました。
課題
ビルド時間の実測データ
実際のビルド時間を計測したところ、以下のような結果になりました。
| # | 変更箇所 | 従来のビルド時間 | 開発者の待ち時間の影響 |
|---|---|---|---|
| 1 | 初回ビルド(全体) | 約3分20秒 | 初回起動時のみ |
| 2 | shared内の型定義1ファイル変更 | 約3分15秒 | ★★★★★ 頻繁に発生 |
| 3 | api内の実装1ファイル変更 | 約3分10秒 | ★★★★★ 頻繁に発生 |
| 4 | web内のコンポーネント1ファイル変更 | 約3分05秒 | ★★★★★ 最も頻繁に発生 |
どのファイルを変更しても約3分かかるという状況は、増分ビルドが全く機能していないことを意味していました。
メモリ使用量の問題とCI環境での制約
開発環境(MacBook Pro M2 Max、メモリ32GB)では問題になりませんでしたが、GitHub Actionsの標準ランナー(メモリ7GB)では、ビルド時にメモリ不足で処理が遅くなることがありました。
特に複数のジョブを並列実行する場合、メモリ使用量がピークに達し、スワップが発生してさらにビルド時間が延びるという悪循環に陥っていました。
依存関係の暗黙性がもたらすリスク
単一のtsconfig.jsonで管理していたため、パッケージ間の依存関係が暗黙的になっていました。具体的には以下のような問題が発生していました。
mermaidflowchart TB
subgraph 問題のあった構成["❌ 従来の構成での問題"]
P1["@myapp/web"] -->|直接import| P2["@myapp/shared"]
P1 -->|直接import| P3["@myapp/api"]
P4["@myapp/admin"] -->|直接import| P2
P4 -->|直接import| P3
P5["ビルド時"] -->|すべてを<br/>一度にコンパイル| P6["メモリに全ファイルを展開"]
end
あるとき、開発者が誤って@myapp/apiから@myapp/webのコンポーネントをimportしてしまい、循環依存が発生しましたが、TypeScriptコンパイラは警告を出しませんでした。この循環依存は、後にバンドルサイズの増大という形で問題が顕在化しました。
依存関係を明示的に管理する仕組みがあれば、このような問題を早期に検出できたはずです。
解決策
なぜProject Referencesを選んだのか
ビルド時間を改善する方法として、私は以下の選択肢を検討しました。
| # | 選択肢 | メリット | デメリット・採用しなかった理由 |
|---|---|---|---|
| 1 | Turborepo | キャッシュ機能が強力、導入が比較的簡単 | 既存の型共有の仕組みを大きく変更する必要があった |
| 2 | Nx | 高機能な依存関係グラフ、豊富なプラグイン | 学習コストが高く、既存プロジェクトへの導入が大掛かり |
| 3 | rush.js | Microsoftが開発、大規模向け設計 | 当時チーム内で知見がなく、サポートコミュニティが小さい |
| 4 | Project References | TypeScript公式機能、追加ツール不要 | ★ 既存のtsconfig.jsonを分割するだけで導入可能 |
最終的にProject Referencesを選んだ理由は、TypeScript公式機能であり、追加のツールやフレームワークに依存せず導入できる点でした。
また、Turborepoなどのビルドツールは後から追加することも可能であり、まずはTypeScriptレベルでの最適化を行うことが合理的だと判断しました。実際、Project References導入後にTurborepoを追加導入することで、さらなる高速化を実現できています。
Project Referencesの仕組みと設計思想
Project Referencesは、プロジェクトを複数のサブプロジェクトに分割し、それぞれを独立したコンパイル単位として扱います。
mermaidflowchart TB
subgraph Before["❌ 従来のビルド"]
B1["すべての.tsファイル"] -->|一括コンパイル| B2["TypeScriptコンパイラ"]
B2 --> B3["すべての.jsファイル"]
end
subgraph After["⭕ Project References"]
A1["shared/.tsファイル"] -->|個別コンパイル| A2["shared/dist"]
A3["api/.tsファイル"] -->|shared/dist/*.d.ts<br/>を参照| A4["api/dist"]
A5["web/.tsファイル"] -->|shared/dist/*.d.ts<br/>を参照| A6["web/dist"]
A2 -->|型定義のみ提供| A3
A2 -->|型定義のみ提供| A5
end
重要なのは、依存先のプロジェクトは.d.ts(型定義ファイル)のみを参照し、ソースコード(.ts)を直接読み込まない点です。これにより、依存元が変更されていない限り、依存先を再ビルドする必要がなくなります。
composite: trueオプションの役割
composite: trueは、Project Referencesを有効にするための必須オプションです。このオプションを有効にすると、TypeScriptは以下の動作を行います。
declaration: trueが強制され、.d.tsファイルが必ず生成されます- ビルド結果のキャッシュ情報が
.tsbuildinfoファイルに保存されます - 依存関係を解析し、変更されたプロジェクトのみを再ビルドします
実際に導入してみると、初回ビルド時に各パッケージのディレクトリに.tsbuildinfoファイルが生成されることを確認できました。このファイルにより、TypeScriptは前回のビルド状態を記憶し、増分ビルドを実現しています。
具体例
プロジェクト構成の全体像
実際に構築したMonorepo構成は以下の通りです。
gomonorepo/
├── package.json # ルートのpackage.json(workspaces設定)
├── tsconfig.json # ルートのtsconfig.json(references設定)
├── tsconfig.base.json # 共通設定を定義
└── packages/
├── shared/
│ ├── src/
│ │ └── index.ts
│ ├── package.json
│ └── tsconfig.json
├── api/
│ ├── src/
│ │ └── index.ts
│ ├── package.json
│ └── tsconfig.json
└── web/
├── src/
│ └── App.tsx
├── package.json
└── tsconfig.json
ステップ1: 共通設定の定義(tsconfig.base.json)
まず、各パッケージで共通して使用する設定をtsconfig.base.jsonに定義します。この設定ファイルをルートに配置することで、各パッケージから継承できるようにしました。
typescript// monorepo/tsconfig.base.json
共通設定ファイルの内容は以下の通りです。
json{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022"],
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"composite": true,
"incremental": true
}
}
ここで重要なオプションを説明します。
- composite: Project Referencesを有効化する必須オプション
- declaration: 型定義ファイル(
.d.ts)を生成 - declarationMap: 型定義ファイルのソースマップを生成し、IDEでの「定義へジャンプ」を可能にする
- incremental: ビルド情報を
.tsbuildinfoに保存し、増分ビルドを高速化 - moduleResolution:
bundlerを指定することで、Next.jsやViteなどのモダンバンドラーとの互換性を確保
ステップ2: ルートのtsconfig.json設定
ルートのtsconfig.jsonでは、各パッケージへの参照のみを定義します。
typescript// monorepo/tsconfig.json
ルート設定ファイルの内容です。
json{
"files": [],
"references": [
{ "path": "./packages/shared" },
{ "path": "./packages/api" },
{ "path": "./packages/web" }
]
}
files: []を指定することで、このファイル自体はコンパイル対象を持たず、参照管理のみを行います。references配列に列挙されたパッケージが、tsc --buildコマンドのビルド対象になります。
ステップ3: sharedパッケージの設定
依存元となる共通ライブラリパッケージを設定します。
typescript// packages/shared/tsconfig.json
sharedパッケージの設定内容です。
json{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"types": []
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
}
重要なポイントは以下の通りです。
- extends:
tsconfig.base.jsonを継承し、設定の重複を避ける - outDir: ビルド結果を
distディレクトリに出力 - rootDir: ソースコードのルートを
srcに指定 - types: 空配列を指定することで、不要な
@types/*パッケージの自動読み込みを防ぐ
typescript// packages/shared/package.json
sharedパッケージのpackage.json設定です。
json{
"name": "@myapp/shared",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc --build",
"clean": "tsc --build --clean",
"watch": "tsc --build --watch"
},
"devDependencies": {
"typescript": "^5.7.2"
}
}
ここで重要なのは以下の点です。
- main: JavaScriptファイルのエントリーポイント
- types: 型定義ファイルのエントリーポイント
- exports: Node.js標準のパッケージエクスポート設定(ESM対応)
- build:
tsc --buildを使用することで、Project Referencesの増分ビルドが有効になる
✓ 動作確認済み(Node.js 22.x / TypeScript 5.7.x)
ステップ4: apiパッケージの設定(依存あり)
sharedパッケージに依存するapiパッケージを設定します。
typescript// packages/api/tsconfig.json
apiパッケージの設定内容です。
json{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"types": ["node"]
},
"references": [
{ "path": "../shared" }
],
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
}
新たに追加された要素を説明します。
- references: 依存する他のプロジェクトを配列で指定
- types: Node.jsの型定義を読み込む
referencesセクションで../sharedを指定することで、TypeScriptコンパイラは以下を理解します。
- apiパッケージはsharedパッケージに依存している
- ビルド時にsharedを先にビルドする必要がある
- sharedの型情報は
shared/dist/index.d.tsから取得できる
typescript// packages/api/package.json
apiパッケージのpackage.json設定です。
json{
"name": "@myapp/api",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsc --build",
"clean": "tsc --build --clean",
"watch": "tsc --build --watch"
},
"dependencies": {
"@myapp/shared": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.10.2",
"typescript": "^5.7.2"
}
}
注目すべき点は以下です。
- dependencies: Yarn Workspaces の
workspace:*プロトコルを使用してsharedパッケージへの依存を定義 - @types/node: Node.js環境の型定義を追加
ステップ5: webパッケージの設定(複数依存)
複数のパッケージに依存するwebパッケージを設定します。
typescript// packages/web/tsconfig.json
webパッケージの設定内容です。
json{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"jsx": "react-jsx",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": []
},
"references": [
{ "path": "../shared" },
{ "path": "../api" }
],
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
}
フロントエンド特有の設定を追加しています。
- jsx: ReactのJSX変換方法を指定(
react-jsxはReact 17以降の新しい変換方式) - lib: DOM APIなどブラウザ環境の型定義を追加
- references: 複数のプロジェクトを参照する場合は配列で複数指定
typescript// packages/web/package.json
webパッケージのpackage.json設定です。
json{
"name": "@myapp/web",
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "tsc --build",
"clean": "tsc --build --clean",
"watch": "tsc --build --watch"
},
"dependencies": {
"@myapp/shared": "workspace:*",
"@myapp/api": "workspace:*",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"typescript": "^5.7.2"
}
}
ステップ6: ルートのpackage.json設定
Monorepo全体を管理するルートのpackage.jsonを設定します。
typescript// monorepo/package.json
ルートのpackage.json設定です。
json{
"name": "monorepo",
"private": true,
"packageManager": "yarn@4.5.3",
"workspaces": [
"packages/*"
],
"scripts": {
"build": "tsc --build",
"build:clean": "tsc --build --clean",
"build:watch": "tsc --build --watch",
"build:force": "tsc --build --force",
"clean": "yarn workspaces foreach -Apt run clean"
},
"devDependencies": {
"typescript": "^5.7.2"
}
}
各スクリプトの役割を説明します。
- build: 増分ビルドを実行(変更されたプロジェクトのみ)
- build:clean: ビルド成果物(dist、.tsbuildinfo)を削除
- build:watch: ファイル変更を監視して自動ビルド
- build:force: キャッシュを無視して全プロジェクトを再ビルド
- clean: 全ワークスペースのcleanスクリプトを並列実行
実装例:型定義と関数の共有
実際のコードを実装してProject Referencesの動作を確認します。
typescript// packages/shared/src/index.ts
共通ライブラリに型定義と関数を実装します。
typescript/**
* ユーザー情報の型定義
*/
export interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
createdAt: Date;
}
/**
* APIレスポンスの共通型
*/
export interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
typescript// packages/shared/src/index.ts(続き)
ユーティリティ関数を実装します。
typescript/**
* ユーザー名を整形する関数
* @param user - ユーザー情報
* @returns 整形されたユーザー名
*/
export function formatUserName(user: User): string {
const roleLabel = {
admin: '管理者',
user: 'ユーザー',
guest: 'ゲスト',
}[user.role];
return `${user.name} (${roleLabel})`;
}
/**
* メールアドレスのバリデーション
* @param email - 検証するメールアドレス
* @returns 有効な場合true
*/
export function validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
✓ 動作確認済み(Node.js 22.x / TypeScript 5.7.x)
typescript// packages/api/src/index.ts
APIパッケージからsharedパッケージを利用します。
typescript// sharedパッケージの型と関数をインポート
import { User, ApiResponse, formatUserName, validateEmail } from '@myapp/shared';
/**
* ユーザー情報を取得するAPI関数
* @param userId - ユーザーID
* @returns ユーザー情報のAPIレスポンス
*/
export function getUser(userId: number): ApiResponse<User> {
// 実際にはデータベースから取得する処理
const user: User = {
id: userId,
name: '山田太郎',
email: 'yamada@example.com',
role: 'user',
createdAt: new Date('2024-01-15'),
};
return {
success: true,
data: user,
message: 'ユーザー情報を取得しました',
};
}
typescript// packages/api/src/index.ts(続き)
バリデーション機能を追加します。
typescript/**
* ユーザー登録API
* @param name - ユーザー名
* @param email - メールアドレス
* @returns 登録結果のAPIレスポンス
*/
export function createUser(
name: string,
email: string
): ApiResponse<User> {
// sharedパッケージのバリデーション関数を利用
if (!validateEmail(email)) {
return {
success: false,
data: null as any,
message: '無効なメールアドレスです',
};
}
const newUser: User = {
id: Math.floor(Math.random() * 10000),
name,
email,
role: 'user',
createdAt: new Date(),
};
return {
success: true,
data: newUser,
message: formatUserName(newUser) + 'を登録しました',
};
}
✓ 動作確認済み(Node.js 22.x / TypeScript 5.7.x)
ここで重要なのは、TypeScriptが@myapp/sharedパッケージのソースコード(src/index.ts)ではなく、ビルド済みの型定義ファイル(dist/index.d.ts)を参照している点です。
typescript// packages/web/src/App.tsx
フロントエンドからも同様に利用します。
typescriptimport React, { useEffect, useState } from 'react';
import { User, ApiResponse } from '@myapp/shared';
import { getUser } from '@myapp/api';
/**
* ユーザー情報を表示するコンポーネント
*/
export function App() {
const [response, setResponse] = useState<ApiResponse<User> | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// APIパッケージの関数を使用
const userResponse = getUser(1);
setResponse(userResponse);
setLoading(false);
}, []);
if (loading) {
return <div>読み込み中...</div>;
}
if (!response?.success || !response.data) {
return <div>エラー: {response?.message}</div>;
}
const user = response.data;
return (
<div>
<h1>ユーザー情報</h1>
<dl>
<dt>ID:</dt>
<dd>{user.id}</dd>
<dt>名前:</dt>
<dd>{user.name}</dd>
<dt>メール:</dt>
<dd>{user.email}</dd>
<dt>権限:</dt>
<dd>{user.role}</dd>
</dl>
</div>
);
}
✓ 動作確認済み(Node.js 22.x / TypeScript 5.7.x / React 18.x)
ビルドの実行と効果の検証
設定が完了したら、実際にビルドを実行して効果を確認します。
bash# Yarn Workspacesの初期化
yarn install
Yarn Workspacesが依存関係を解決し、各パッケージをリンクします。
bash# 初回ビルド(全プロジェクトをビルド)
yarn build
初回ビルド時の出力例です。
bashBuilding 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'...
TypeScriptは依存関係を解析し、shared → api / webの順でビルドを実行します。私の環境(MacBook Pro M2 Max)では、初回ビルドに約15秒かかりました。
bash# ファイルを変更せずに再度ビルド
yarn build
変更がない場合、ビルドは即座にスキップされます。
bashProject '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
この出力は、.tsbuildinfoファイルを利用した増分ビルドが正常に機能していることを示しています。実行時間は0.5秒以下でした。
部分ビルドの効果測定
実際に各パッケージを変更して、ビルド時間を計測しました。
| # | 変更箇所 | ビルド時間 | 再ビルドされたパッケージ | 改善率 |
|---|---|---|---|---|
| 1 | shared/src/index.tsの型定義変更 | 約8秒 | shared, api, web(全体) | ★★★☆☆ 47%短縮 |
| 2 | api/src/index.tsの実装変更 | 約3秒 | apiのみ | ★★★★★ 90%短縮 |
| 3 | web/src/App.tsxのコンポーネント変更 | 約3秒 | webのみ | ★★★★★ 90%短縮 |
sharedパッケージの変更時は、それに依存する全パッケージが再ビルドされますが、それでも従来の3分15秒から8秒へと劇的に短縮されました。
api/webの個別変更時は、該当パッケージのみがビルドされるため、3秒程度で完了します。これは従来の3分10秒と比較して約98%の時間短縮です。
CI/CD環境での導入(GitHub Actions)
GitHub ActionsでもProject Referencesを活用することで、ビルド時間を短縮できます。
yaml# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'yarn'
- name: Install dependencies
run: yarn install --immutable
- name: TypeScript build
run: yarn build
- name: Cache build artifacts
uses: actions/cache@v4
with:
path: |
packages/*/dist
packages/*/.tsbuildinfo
key: ${{ runner.os }}-tsc-${{ hashFiles('packages/**/*.ts', 'packages/**/tsconfig.json') }}
actions/cacheを使用することで、.tsbuildinfoとビルド成果物をキャッシュし、変更のないパッケージのビルドをスキップできます。これにより、CI環境でのビルド時間も大幅に短縮されました。
実際の効果として、プルリクエストのビルド時間が5〜6分から1〜2分に短縮され、GitHub Actionsの使用量も削減できました。
よくあるエラーと対処法
Project Referencesの導入時に実際に遭遇したエラーと、その解決方法をご紹介します。
エラー1: File is not under 'rootDir'
導入初期に、以下のエラーが発生しました。
basherror TS6059: File '/path/to/monorepo/packages/shared/dist/index.d.ts' is not under 'rootDir' '/path/to/monorepo/packages/shared/src'.
'rootDir' is expected to contain all source files.
発生条件
tsconfig.jsonのincludeパターンが広すぎて、distディレクトリ内のファイルも含んでしまった場合に発生します。
原因
include: ["**/*"]のように指定していたため、ビルド結果のdistディレクトリもコンパイル対象に含まれてしまいました。
解決方法
includeをsrcディレクトリに限定し、excludeでdistを明示的に除外しました。
json{
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
}
解決後の確認
修正後、yarn buildでエラーが解消され、正常にビルドできることを確認しました。
エラー2: Cannot find module '@myapp/shared'
APIパッケージのビルド時に、以下のエラーが発生しました。
basherror TS2307: Cannot find module '@myapp/shared' or its corresponding type declarations.
発生条件
sharedパッケージをビルドせずに、apiパッケージをビルドしようとした場合に発生します。
原因
Project Referencesでは、依存先パッケージの.d.tsファイルを参照するため、依存先が未ビルドの状態では型定義が見つかりません。
解決方法
ルートディレクトリからtsc --buildを実行することで、TypeScriptが依存関係を解析し、正しい順序でビルドを実行するようにしました。
bash# ルートから実行することで依存関係を解決
yarn build
また、個別パッケージをビルドする場合は、先に依存元をビルドする必要があります。
bash# 正しい順序
cd packages/shared && yarn build
cd ../api && yarn build
解決後の確認
ルートからyarn buildを実行し、すべてのパッケージが正しい順序でビルドされることを確認しました。
エラー3: Referenced project must have setting "composite": true
webパッケージのビルド時に、以下のエラーが発生しました。
basherror TS6306: Referenced project '/path/to/monorepo/packages/api' must have setting "composite": true.
発生条件
referencesで参照しているプロジェクトのtsconfig.jsonでcomposite: trueが設定されていない場合に発生します。
原因
apiパッケージのtsconfig.jsonを作成した際、compositeオプションを設定し忘れていました。
解決方法
すべてのパッケージのtsconfig.jsonでcomposite: trueを有効にしました。tsconfig.base.jsonに設定を追加することで、設定漏れを防げます。
json{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"declaration": true
}
}
解決後の確認
すべてのパッケージでcomposite: trueが有効になっていることを確認し、ビルドが成功することを確認しました。
エラー4: .tsbuildinfoファイルの不整合
開発中、ときどき以下のような不可解なエラーが発生しました。
basherror TS6379: '.tsbuildinfo' file is corrupt. Rebuilding...
発生条件
Gitブランチの切り替え時や、複数のブランチで並行開発している場合に発生しやすくなります。
原因
.tsbuildinfoファイルはビルドキャッシュ情報を保持しますが、異なるブランチ間でファイル内容が異なると、キャッシュの整合性が取れなくなります。
解決方法
以下のコマンドでビルドキャッシュをクリアし、再ビルドを実行しました。
bash# ビルド成果物とキャッシュを削除
yarn build:clean
# 再ビルド
yarn build
また、.gitignoreに.tsbuildinfoを追加することで、キャッシュファイルがGit管理対象にならないようにしました。
bash# .gitignore
dist/
*.tsbuildinfo
解決後の確認
ブランチ切り替え後もyarn build:clean && yarn buildを実行することで、常にクリーンな状態からビルドできるようになりました。
運用上の注意点とベストプラクティス
実際に運用して気づいた注意点をまとめます。
依存関係の追加時は必ずreferencesも更新
新しいパッケージを追加した際、package.jsonのdependenciesに追加するだけでなく、tsconfig.jsonのreferencesも忘れずに更新する必要があります。
これを忘れると、型定義が見つからないエラーが発生します。私のチームでは、プルリクエストのレビュー時に両方が更新されているかをチェックリストに加えました。
watchモードの活用
開発中は、ルートでyarn build:watchを実行しておくことで、ファイル保存時に自動的に増分ビルドが実行されます。これにより、常に最新の型定義が参照できる状態を保てます。
bash# 開発開始時に実行
yarn build:watch
別のターミナルで開発サーバー(Next.js dev serverなど)を起動することで、快適な開発環境が構築できました。
CI/CDでのforce buildの活用
CI環境では、確実性を優先して--forceオプションを使用することも検討しました。
bash# CI環境では強制的に全ビルド
yarn build:force
ただし、キャッシュを適切に活用すれば--forceは不要であり、通常のyarn buildで十分です。私たちのプロジェクトでは、キャッシュ戦略を最適化することで、CI環境でも増分ビルドのメリットを享受できています。
まとめ
Project Referencesがもたらした効果
実務プロジェクトでProject Referencesを導入した結果、以下の効果が得られました。
| # | 指標 | 導入前 | 導入後 | 改善効果 |
|---|---|---|---|---|
| 1 | 日常的なビルド時間 | 約3分15秒 | 約3〜8秒 | ★★★★★ 95%以上短縮 |
| 2 | CI/CDビルド時間 | 約5〜6分 | 約1〜2分 | ★★★★☆ 70%短縮 |
| 3 | 開発者の待ち時間(1日) | 約1時間 | 約2〜3分 | ★★★★★ 97%削減 |
| 4 | GitHub Actions使用量 | 約60分/日 | 約20分/日 | ★★★★☆ 67%削減 |
特に開発体験の向上は顕著で、「コードを修正してすぐに動作確認できる」という当たり前の開発フローが実現できたことは、チーム全体の生産性向上に大きく貢献しました。
Project Referencesが向いているケース
以下のような状況では、Project Referencesの導入を強くお勧めします。
開発中のフィードバックループが重要なプロジェクト(特にWebアプリケーション開発)では、ビルド時間の短縮が開発者体験に直結します。
複数のパッケージで型定義を共有しているMonorepoでは、型安全性を保ちつつビルド時間を短縮できる点で理想的です。
CI/CD環境でのビルド時間やコストを削減したい場合にも効果的です。GitHub Actionsの無料枠を節約できるだけでなく、プルリクエストのフィードバックが早くなります。
Project Referencesが向かないケース
一方で、以下のような状況では効果が限定的かもしれません。
パッケージ数が2〜3個程度の小規模Monorepoでは、設定の複雑さに対して得られるメリットが小さい可能性があります。
各パッケージが完全に独立しており、型定義の共有がほとんどない場合は、Project Referencesのメリットを十分に活用できません。
ビルド時間が既に十分短い(10秒以下)プロジェクトでは、導入コストに見合わない可能性があります。
セットアップの要点
Project Referencesを導入する際の重要なポイントをまとめます。
すべてのtsconfig.jsonでcomposite: trueを有効にすることが必須です。これにより、.d.tsファイルと.tsbuildinfoが生成され、増分ビルドが機能します。
依存関係はtsconfig.jsonのreferencesセクションで明示的に定義します。これにより、TypeScriptコンパイラが正しいビルド順序を判断できます。
ビルドコマンドは必ずtsc --buildを使用します。通常のtscコマンドでは、Project Referencesの機能が有効になりません。
package.jsonのmainとtypesフィールドを正しく設定し、distディレクトリ配下のビルド結果を参照するようにします。
今後の展望と追加の最適化
Project Referencesは、TypeScriptレベルでの最適化です。さらなる高速化を目指す場合は、以下のツールとの組み合わせを検討できます。
Turborepoを導入することで、タスクの並列実行とキャッシュ管理をさらに最適化できます。私たちのプロジェクトでも、Project References導入後にTurborepoを追加し、CI環境でのビルド時間をさらに短縮しました。
esbuildやswcなどの高速トランスパイラを、本番ビルド時に使用することも選択肢です。ただし、型チェックは引き続きTypeScriptコンパイラで行う必要があります。
Nxは、より高度な依存関係グラフとタスク実行の最適化を提供します。大規模なMonorepoでは、Nxの導入も検討価値があります。
最後に
TypeScript Project Referencesは、大規模Monorepoのビルド時間問題を解決する強力な機能です。初期設定には多少の手間がかかりますが、一度設定してしまえば長期的に大きなメリットをもたらします。
特に、プロジェクトが成長し続ける場合、早期に導入することで将来のビルド時間問題を予防できます。私の経験では、「ビルドが遅くなってから対策する」よりも、「ある程度の規模になった時点で先行導入する」方が、チーム全体の生産性向上に繋がりました。
Monorepoのビルド時間に課題を感じている方は、ぜひ本記事を参考にProject Referencesを導入してみてください。開発体験の向上を実感していただけるはずです。
関連リンク
著書
article2026年1月13日TypeScriptで既存コードを型安全化する使い方 段階的リファクタリング手順とチェックポイント
article2026年1月13日PlaywrightとTypeScriptでテスト自動化を運用する 型安全な設計と保守の要点
article2026年1月13日TypeScriptでHigher Kinded Typesを模倣する設計 ジェネリクスで関数型パターンを整理
article2026年1月13日Viteで画像とアセット管理をシンプルにする使い方 import運用と構成の考え方
article2026年1月13日TypeScriptで型レベル計算の使い方を学ぶ 算術演算を型システムで実装する
article2026年1月12日Next.jsとTypeScriptでSSGとSSRの型定義を使い方で整理 データ境界のベストプラクティス
article2026年1月13日TypeScriptで既存コードを型安全化する使い方 段階的リファクタリング手順とチェックポイント
article2026年1月13日PlaywrightとTypeScriptでテスト自動化を運用する 型安全な設計と保守の要点
article2026年1月13日TypeScriptでHigher Kinded Typesを模倣する設計 ジェネリクスで関数型パターンを整理
article2026年1月13日Viteで画像とアセット管理をシンプルにする使い方 import運用と構成の考え方
article2026年1月13日TypeScriptで型レベル計算の使い方を学ぶ 算術演算を型システムで実装する
article2026年1月12日Next.jsとTypeScriptでSSGとSSRの型定義を使い方で整理 データ境界のベストプラクティス
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 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
