T-CREATOR

TypeScript Project References 入門:大規模 Monorepo で高速ビルドを実現する設定手順

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/shared505 秒
2@myapp/api20020 秒
3@myapp/web30030 秒
4@myapp/admin15015 秒

従来の方法では、@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 パッケージは他のすべてのパッケージから参照され、webadminapi パッケージにも依存しています。

従来の 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%(ややオーバーヘッド)
2shared のみ変更70 秒25 秒★★★★★ 64% 改善
3api のみ変更70 秒20 秒★★★★★ 71% 改善
4web のみ変更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 パッケージは sharedapi の両方に依存する例として設定しましょう。

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.jsonreferences を追加して、すべてのプロジェクトを管理できるようにしましょう。

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 に依存する apiweb がビルドされることがわかります。

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 の外にあるファイルをインポートしようとした場合

解決方法:

  1. tsconfig.jsoninclude パターンを確認します
  2. rootDiroutDir の設定が適切か確認します
  3. 必要に応じて include パターンを調整します

エラー 2: "Cannot find module"

エラーメッセージ:

luaerror TS2307: Cannot find module '@myapp/shared' or its corresponding type declarations.

発生条件: 参照プロジェクトがビルドされていない、または references の設定が不足している場合

解決方法:

  1. tsconfig.jsonreferences セクションに依存プロジェクトを追加します
  2. 依存プロジェクトを先にビルドします(yarn build がルートから実行されているか確認)
  3. package.jsondependencies にも依存パッケージが記載されているか確認します

エラー 3: "composite が有効でない"

エラーメッセージ:

goerror TS6306: Referenced project '/path/to/project' must have setting "composite": true.

発生条件: 参照されるプロジェクトの tsconfig.jsoncompositetrue に設定されていない場合

解決方法:

すべてのプロジェクトの 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型安全性の維持依存プロジェクトの型情報を確実に参照できます

実装のステップ

  1. ルートの tsconfig.json でベース設定を定義
  2. 各パッケージに個別の tsconfig.json を作成し、composite: true を設定
  3. references セクションで依存関係を定義
  4. package.json にビルドスクリプトを追加
  5. tsc --build コマンドでビルドを実行

Project References は初期設定に多少の手間がかかりますが、一度設定してしまえば長期的に大きなメリットをもたらします。特に、継続的に成長するプロジェクトでは、早期に導入することをお勧めいたします。

Monorepo の規模が大きくなり、ビルド時間に悩んでいる方は、ぜひ本記事を参考に Project References を導入してみてください。開発体験の向上を実感していただけるはずです。

関連リンク