T-CREATOR

<div />

TypeScript Project Referencesのセットアップ手順 大規模Monorepoで高速ビルドするtsconfig設定

2025年12月24日
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秒初回起動時のみ
2shared内の型定義1ファイル変更約3分15秒★★★★★ 頻繁に発生
3api内の実装1ファイル変更約3分10秒★★★★★ 頻繁に発生
4web内のコンポーネント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を選んだのか

ビルド時間を改善する方法として、私は以下の選択肢を検討しました。

#選択肢メリットデメリット・採用しなかった理由
1Turborepoキャッシュ機能が強力、導入が比較的簡単既存の型共有の仕組みを大きく変更する必要があった
2Nx高機能な依存関係グラフ、豊富なプラグイン学習コストが高く、既存プロジェクトへの導入が大掛かり
3rush.jsMicrosoftが開発、大規模向け設計当時チーム内で知見がなく、サポートコミュニティが小さい
4Project ReferencesTypeScript公式機能、追加ツール不要★ 既存の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コンパイラは以下を理解します。

  1. apiパッケージはsharedパッケージに依存している
  2. ビルド時にsharedを先にビルドする必要がある
  3. 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は依存関係を解析し、sharedapi / 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秒以下でした。

部分ビルドの効果測定

実際に各パッケージを変更して、ビルド時間を計測しました。

#変更箇所ビルド時間再ビルドされたパッケージ改善率
1shared/src/index.tsの型定義変更約8秒shared, api, web(全体)★★★☆☆ 47%短縮
2api/src/index.tsの実装変更約3秒apiのみ★★★★★ 90%短縮
3web/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.jsonincludeパターンが広すぎて、distディレクトリ内のファイルも含んでしまった場合に発生します。

原因

include: ["**​/​*"]のように指定していたため、ビルド結果のdistディレクトリもコンパイル対象に含まれてしまいました。

解決方法

includesrcディレクトリに限定し、excludedistを明示的に除外しました。

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.jsoncomposite: trueが設定されていない場合に発生します。

原因

apiパッケージのtsconfig.jsonを作成した際、compositeオプションを設定し忘れていました。

解決方法

すべてのパッケージのtsconfig.jsoncomposite: 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.jsondependenciesに追加するだけでなく、tsconfig.jsonreferencesも忘れずに更新する必要があります。

これを忘れると、型定義が見つからないエラーが発生します。私のチームでは、プルリクエストのレビュー時に両方が更新されているかをチェックリストに加えました。

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%以上短縮
2CI/CDビルド時間約5〜6分約1〜2分★★★★☆ 70%短縮
3開発者の待ち時間(1日)約1時間約2〜3分★★★★★ 97%削減
4GitHub 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.jsoncomposite: trueを有効にすることが必須です。これにより、.d.tsファイルと.tsbuildinfoが生成され、増分ビルドが機能します。

依存関係はtsconfig.jsonreferencesセクションで明示的に定義します。これにより、TypeScriptコンパイラが正しいビルド順序を判断できます。

ビルドコマンドは必ずtsc --buildを使用します。通常のtscコマンドでは、Project Referencesの機能が有効になりません。

package.jsonmaintypesフィールドを正しく設定し、distディレクトリ配下のビルド結果を参照するようにします。

今後の展望と追加の最適化

Project Referencesは、TypeScriptレベルでの最適化です。さらなる高速化を目指す場合は、以下のツールとの組み合わせを検討できます。

Turborepoを導入することで、タスクの並列実行とキャッシュ管理をさらに最適化できます。私たちのプロジェクトでも、Project References導入後にTurborepoを追加し、CI環境でのビルド時間をさらに短縮しました。

esbuildswcなどの高速トランスパイラを、本番ビルド時に使用することも選択肢です。ただし、型チェックは引き続きTypeScriptコンパイラで行う必要があります。

Nxは、より高度な依存関係グラフとタスク実行の最適化を提供します。大規模なMonorepoでは、Nxの導入も検討価値があります。

最後に

TypeScript Project Referencesは、大規模Monorepoのビルド時間問題を解決する強力な機能です。初期設定には多少の手間がかかりますが、一度設定してしまえば長期的に大きなメリットをもたらします。

特に、プロジェクトが成長し続ける場合、早期に導入することで将来のビルド時間問題を予防できます。私の経験では、「ビルドが遅くなってから対策する」よりも、「ある程度の規模になった時点で先行導入する」方が、チーム全体の生産性向上に繋がりました。

Monorepoのビルド時間に課題を感じている方は、ぜひ本記事を参考にProject Referencesを導入してみてください。開発体験の向上を実感していただけるはずです。

関連リンク

著書

とあるクリエイター

フロントエンドエンジニア Next.js / React / TypeScript / Node.js / Docker / AI Coding

;