TypeScriptでモノレポ管理をセットアップする手順 プロジェクト分割と依存関係制御の実践
TypeScript でモノレポを構築する際、「とりあえず Yarn Workspaces を入れて動いた」で満足していませんか。実際の開発では、ビルド順序が壊れたり、変更していないパッケージまで再ビルドされたり、CI で 10 分以上かかったりと、運用で破綻するケースが多発します。
本記事は、TypeScript モノレポのセットアップから運用まで、実務で問題にならない設計判断を整理したものです。Project References を中心に、tsconfig.json の階層化、ビルド順序の制御、差分ビルドによる CI/CD 高速化まで、実際に検証した内容をもとに解説します。
「初回ビルドは通るけど 2 回目で壊れる」「型チェックは通るのにビルドが失敗する」といった問題を避けるため、静的型付けの恩恵を最大限に活かしつつ、運用で破綻しないセットアップ手順を提示します。
検証環境
- OS: macOS Sequoia 15.2
- Node.js: 22.21.0 (LTS)
- TypeScript: 5.9.3
- 主要パッケージ:
- yarn: 4.12.0
- turbo: 2.3.3
- 検証日: 2026 年 1 月 10 日
背景:TypeScript モノレポが必要になる実務場面
モノレポ管理とは何か
この章では、モノレポが解決する実務上の課題と、TypeScript との相性について理解できます。
モノレポ(Monorepo)とは、複数の関連プロジェクトを単一リポジトリで管理する手法です。フロントエンド、バックエンド、共通ライブラリを同じリポジトリに配置し、依存関係を明示的に管理します。
対義語はマルチレポ(Polyrepo)で、各プロジェクトを個別リポジトリで管理する従来の方法です。
bashmonorepo/
├── packages/
│ ├── web/ # Next.js アプリ
│ ├── api/ # Express API
│ └── shared/ # 共通型・ユーティリティ
├── package.json
└── tsconfig.json
TypeScript で型を共有する必要性
TypeScript プロジェクトでは、フロントエンドとバックエンドで同じ型定義を使いたい場面が頻繁に発生します。たとえば API レスポンスの型、データベースモデルの型、バリデーションスキーマなどです。
マルチレポでは、これらの型を npm パッケージとして公開し、各プロジェクトでインストールする必要がありました。しかし、型定義 1 つ変更するたびに「パッケージ公開 → バージョンアップ → 各リポジトリで yarn upgrade」という手順を踏むのは非効率です。
モノレポでは、共通の型定義を packages/shared に配置し、他のパッケージから直接参照できます。型の変更は即座に全体に反映され、静的型付けの恩恵を最大限に活かせます。
typescript// packages/shared/src/types.ts
export interface User {
id: string;
name: string;
email: string;
}
// packages/api/src/user.ts
import { User } from "@myapp/shared";
// packages/web/src/components/UserProfile.tsx
import { User } from "@myapp/shared";
実務で起きる依存関係の混乱
実際の開発では、複数プロジェクトで同じライブラリの異なるバージョンを使ってしまうことがあります。検証中、プロジェクト A は React 18.2、プロジェクト B は React 18.3 を使っており、型定義の不整合でビルドエラーが発生しました。
モノレポでは、ルートの package.json で依存関係を一元管理できるため、このような不整合を防げます。
以下は、モノレポとマルチレポの比較です。
| 項目 | マルチレポ | モノレポ |
|---|---|---|
| 型の共有 | npm パッケージ公開が必要 | 直接参照可能 |
| 依存関係 | 各リポジトリで個別管理 | ルートで一元管理 |
| ビルド | 各リポジトリで個別実行 | 依存順序で自動制御 |
| CI/CD | 各リポジトリで設定 | 差分検知で高速化可能 |
| リファクタリング | 影響範囲の把握が困難 | IDE で一括変更可能 |
つまずきポイント
- モノレポは「すべてを 1 つにまとめる」ことが目的ではありません。関連するプロジェクト群を効率的に管理するための手法です。
- 無関係なプロジェクトまで同じリポジトリに入れると、かえって管理が複雑になります。
課題:運用で破綻するモノレポの典型例
Yarn Workspaces だけでは不十分な理由
この章では、Workspaces のみでセットアップした場合に起きる実際の問題を理解できます。
Yarn Workspaces は、モノレポの依存関係を解決する仕組みです。packages/web が packages/shared に依存する場合、以下のように package.json に記述します。
json{
"name": "@myapp/web",
"dependencies": {
"@myapp/shared": "workspace:*"
}
}
しかし、Workspaces だけではビルド順序が保証されません。実際の検証で、packages/web をビルドする際に packages/shared がまだビルドされておらず、型定義ファイル(.d.ts)が見つからないエラーが発生しました。
bash# エラー例
error TS2307: Cannot find module '@myapp/shared' or its corresponding type declarations.
Workspaces は依存関係のインストールを管理しますが、ビルド順序は管理しません。この問題を解決するには、TypeScript の Project References が必要です。
初回ビルドは通るが 2 回目で壊れる問題
業務で遭遇した典型的な問題として、「初回ビルドは成功するが、2 回目以降で型エラーが出る」というケースがありました。
原因は、tsc が生成する .tsbuildinfo ファイル(増分ビルド用のキャッシュ)と、実際の依存関係が不整合を起こしていたためです。composite: true を設定せずに Project References を使うと、この現象が発生します。
json// ❌ 悪い例:composite が抜けている
{
"compilerOptions": {
"outDir": "./dist"
},
"references": [{ "path": "../shared" }]
}
json// ⭕ 正しい例
{
"compilerOptions": {
"composite": true,
"outDir": "./dist"
},
"references": [{ "path": "../shared" }]
}
CI で毎回全パッケージがビルドされる無駄
CI/CD で「変更したのは 1 ファイルだけなのに、全パッケージが再ビルドされて 10 分かかる」という問題も頻発します。
これは、差分検知の仕組みがないためです。tsc --build には増分ビルド機能がありますが、CI では毎回クリーンな環境で実行されるため、キャッシュが効きません。
実務では、Turborepo や Nx などのビルドツールを使い、Git のコミット履歴から変更されたパッケージを検知して、必要な部分だけをビルドする仕組みが必要です。
以下は、差分ビルドの有無による CI 時間の比較です。
mermaidflowchart LR
commit["コミット"] --> detect["変更検知"]
detect --> cache["キャッシュ判定"]
cache --> build1["必要なパッケージのみビルド"]
cache --> skip["変更なしのパッケージはスキップ"]
build1 --> fast["高速化"]
skip --> fast
差分ビルドがない場合、毎回すべてのパッケージをビルドするため、CI 時間が 5〜10 分かかります。差分ビルドを導入すると、変更されたパッケージのみをビルドするため、1〜2 分に短縮できます。
つまずきポイント
- tsc --build --incremental は、ローカルでは有効ですが、CI では環境が毎回リセットされるため効果が薄いです。
- CI でキャッシュを活用するには、Turborepo のリモートキャッシュや GitHub Actions の cache アクションが必要です。
解決策と判断:Project References を中心とした設計
Project References とは何か
この章では、TypeScript の Project References を使った依存関係の明示と、ビルド順序の制御方法を理解できます。
Project References は、TypeScript 3.0 で導入された、複数の tsconfig.json を関連付ける機能です。パッケージ間の依存関係を明示し、tsc --build で正しい順序でビルドできます。
json// packages/web/tsconfig.json
{
"compilerOptions": {
"composite": true,
"outDir": "./dist"
},
"references": [{ "path": "../shared" }]
}
この設定により、tsc --build は以下の順序で実行されます。
- packages/shared をビルド
- shared のビルド完了後、packages/web をビルド
composite: true の役割と必須性
composite: true は、Project References を使う際に必須のオプションです。これを設定すると、以下が有効になります。
- 宣言ファイル(.d.ts)の自動生成
- .tsbuildinfo ファイルによる増分ビルド
- rootDir の自動推定
業務では、composite: true を設定し忘れて「ビルドは通るが型が解決されない」というエラーが頻発しました。
json{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
}
}
ルート tsconfig.json の設計判断
ルートの tsconfig.json は、全パッケージ共通の設定を定義します。実務では、以下の方針で設計しました。
- 共通設定のみをルートに配置:target、module、strict など
- パッケージ固有の設定は各 tsconfig.json に委譲:jsx、lib など
- references でパッケージを列挙:ビルド順序を制御
json// tsconfig.json(ルート)
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true
},
"files": [],
"references": [
{ "path": "./packages/shared" },
{ "path": "./packages/api" },
{ "path": "./packages/web" }
]
}
files: [] の記述は重要です。これにより、ルートの tsconfig.json はビルド対象を持たず、references のみを管理する役割に限定されます。
以下は、tsconfig.json の階層構造を示した図です。
mermaidflowchart TD
root["ルート tsconfig.json<br/>共通設定のみ"]
shared["packages/shared/tsconfig.json<br/>composite: true"]
api["packages/api/tsconfig.json<br/>composite: true"]
web["packages/web/tsconfig.json<br/>composite: true"]
root --> shared
root --> api
root --> web
api --> shared
web --> shared
採用しなかった設計とその理由
検証中、以下の設計も試しましたが、採用しませんでした。
パスマッピング(paths)のみで解決する案
json{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@myapp/shared": ["packages/shared/src"]
}
}
}
この方法は、TypeScript の型解決には有効ですが、ビルド順序が保証されません。実際の検証で、packages/shared のビルド前に packages/web がビルドされ、型定義ファイルが見つからないエラーが発生しました。
paths は型解決のためのショートカットであり、依存関係の管理には Project References が必要です。
Lerna によるパッケージ管理
Lerna は、モノレポのパッケージ管理ツールですが、2022 年以降メンテナンスが停滞しています。Yarn Workspaces + Turborepo の組み合わせの方が、現在の TypeScript プロジェクトには適しています。
つまずきポイント
- paths と Project References は役割が異なります。paths は型解決、references はビルド順序の制御です。
- 両方を併用することも可能ですが、references だけで十分なケースが多いです。
具体例:実際に動作するセットアップ手順
ディレクトリ構造の設計
この章では、実務で採用した具体的なディレクトリ構造と、各ファイルの役割を理解できます。
以下は、実際に検証した構成です。
gomyapp/
├── packages/
│ ├── shared/
│ │ ├── src/
│ │ │ ├── types.ts
│ │ │ └── index.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── api/
│ │ ├── src/
│ │ │ ├── server.ts
│ │ │ └── routes/
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── web/
│ ├── src/
│ │ ├── App.tsx
│ │ └── components/
│ ├── package.json
│ └── tsconfig.json
├── package.json
├── tsconfig.json
└── turbo.json
ルート package.json の設定
動作確認済みの設定です。
json{
"name": "myapp",
"private": true,
"workspaces": ["packages/*"],
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --parallel",
"typecheck": "tsc --build",
"clean": "turbo run clean && rm -rf node_modules/.cache"
},
"devDependencies": {
"typescript": "5.9.3",
"turbo": "2.3.3"
},
"packageManager": "yarn@4.12.0"
}
packageManager フィールドは、Corepack による Yarn バージョンの固定に使います。これにより、チーム全員が同じバージョンの Yarn を使うことが保証されます。
共通パッケージ(shared)の設定
packages/shared は、他のパッケージから参照される型定義とユーティリティを含みます。
json// packages/shared/package.json
{
"name": "@myapp/shared",
"version": "0.1.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"clean": "rm -rf dist"
},
"devDependencies": {
"typescript": "5.9.3"
}
}
json// packages/shared/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}
extends により、ルートの tsconfig.json から共通設定を継承します。composite、declaration、declarationMap は、Project References に必須の設定です。
API パッケージの設定
packages/api は、Express などのバックエンドフレームワークを使う想定です。
json// packages/api/package.json
{
"name": "@myapp/api",
"version": "0.1.0",
"main": "./dist/server.js",
"scripts": {
"build": "tsc",
"dev": "tsx watch src/server.ts",
"clean": "rm -rf dist"
},
"dependencies": {
"@myapp/shared": "workspace:*",
"express": "^4.21.2"
},
"devDependencies": {
"@types/express": "^5.0.0",
"tsx": "^4.19.2",
"typescript": "5.9.3"
}
}
workspace:* により、ワークスペース内の最新バージョンを参照します。
json// packages/api/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"references": [{ "path": "../shared" }]
}
references で shared への依存を明示します。これにより、tsc --build は shared → api の順にビルドします。
Web パッケージの設定
packages/web は、React や Next.js などのフロントエンドフレームワークを使う想定です。
json// packages/web/package.json
{
"name": "@myapp/web",
"version": "0.1.0",
"scripts": {
"build": "tsc && vite build",
"dev": "vite",
"clean": "rm -rf dist"
},
"dependencies": {
"@myapp/shared": "workspace:*",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "5.9.3",
"vite": "^6.0.7"
}
}
json// packages/web/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"jsx": "react-jsx",
"lib": ["ES2022", "DOM"],
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"references": [{ "path": "../shared" }]
}
jsx: "react-jsx" は、React 17 以降の新しい JSX 変換に対応します。
Turborepo によるビルドパイプライン設定
Turborepo は、モノレポのタスクランナーです。依存関係を解析し、並列ビルドとキャッシュを提供します。
json// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"typecheck": {
"dependsOn": ["^build"]
},
"clean": {
"cache": false
}
}
}
dependsOn: ["^build"] により、依存パッケージのビルドが先に実行されます。outputs はキャッシュ対象のディレクトリです。
以下は、Turborepo のビルドフローを示した図です。
mermaidflowchart TD
start["turbo run build"] --> detect["依存関係の解析"]
detect --> shared["packages/shared のビルド"]
shared --> parallel["並列実行"]
parallel --> api["packages/api のビルド"]
parallel --> web["packages/web のビルド"]
api --> cache1["キャッシュに保存"]
web --> cache2["キャッシュに保存"]
実際のビルドコマンドと動作確認
動作確認済みのコマンドです。
bash# 初回セットアップ
corepack enable
yarn install
# 型チェック
yarn typecheck
# 全パッケージのビルド
yarn build
# 開発モード(並列実行)
yarn dev
初回ビルドの出力例です。
bash$ yarn build
• Packages in scope: @myapp/api, @myapp/shared, @myapp/web
• Running build in 3 packages
@myapp/shared:build: cache miss, executing 1a2b3c4d
@myapp/shared:build: compiled successfully
@myapp/api:build: cache miss, executing 5e6f7g8h
@myapp/api:build: compiled successfully
@myapp/web:build: cache miss, executing 9i0j1k2l
@myapp/web:build: compiled successfully
Tasks: 3 successful, 3 total
Cached: 0 cached, 3 total
Time: 4.2s
2 回目以降はキャッシュが効きます。
bash$ yarn build
• Packages in scope: @myapp/api, @myapp/shared, @myapp/web
• Running build in 3 packages
@myapp/shared:build: cache hit, replaying output 1a2b3c4d
@myapp/api:build: cache hit, replaying output 5e6f7g8h
@myapp/web:build: cache hit, replaying output 9i0j1k2l
Tasks: 3 successful, 3 total
Cached: 3 cached, 3 total
Time: 0.3s >>> FULL TURBO
CI/CD での差分ビルド設定
GitHub Actions での設定例です。
yaml# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 22.21.0
- run: corepack enable
- name: キャッシュの復元
uses: actions/cache@v4
with:
path: |
node_modules
.turbo
key: ${{ runner.os }}-${{ hashFiles('yarn.lock') }}
- run: yarn install --immutable
- name: 変更されたパッケージのみビルド
run: yarn turbo run build --filter="...[origin/main]"
- name: 型チェック
run: yarn typecheck
--filter="...[origin/main]" により、main ブランチから変更されたパッケージとその依存パッケージのみがビルドされます。
つまずきポイント
- turbo.json の dependsOn: ["^build"] は、依存パッケージのビルドを意味します。dependsOn: ["build"] は、同じパッケージの build タスクに依存する意味になるため、誤りです。
- GitHub Actions で fetch-depth: 0 を設定しないと、Git の履歴が浅くなり、差分検知が正しく動作しません。
まとめ:運用で破綻しないモノレポ管理の判断基準
TypeScript モノレポのセットアップでは、Yarn Workspaces だけでなく、Project References による依存関係の明示が必須です。composite: true を設定し、tsconfig.json の階層化を適切に行うことで、ビルド順序が保証され、型安全性が維持されます。
CI/CD では、Turborepo などのツールを使い、変更されたパッケージのみをビルドする仕組みが重要です。キャッシュを活用することで、ビルド時間を数分から数十秒に短縮できます。
ただし、モノレポはすべてのプロジェクトに適しているわけではありません。関連性の低いプロジェクトまで同じリポジトリに入れると、かえって管理コストが増大します。以下の条件に当てはまる場合に、モノレポが有効です。
- フロントエンドとバックエンドで型定義を共有したい
- 複数のプロジェクトで同じライブラリバージョンを使いたい
- リファクタリングの影響範囲を把握しやすくしたい
- CI/CD を統一して管理したい
逆に、以下の場合はマルチレポの方が適しています。
- プロジェクト間に依存関係がない
- チームが完全に分離されている
- リリースサイクルが大きく異なる
実務では、段階的に導入することをおすすめします。まず共通ライブラリだけをモノレポ化し、運用が安定してから他のパッケージを追加する方法が、失敗が少ないです。
関連リンク
著書
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月13日TypeScriptで実行時バリデーション自動生成を設計する 型と実行時チェックを整合させる
articleNext.js・React Server Componentsが危険?async_hooksの脆弱性CVE-2025-59466を徹底解説
article【緊急】2026年1月13日発表 Node.js 脆弱性8件の詳細と対策|HTTP/2・async_hooks のDoS問題を解説
article2026年1月13日TypeScriptで既存コードを型安全化する使い方 段階的リファクタリング手順とチェックポイント
article2026年1月13日PlaywrightとTypeScriptでテスト自動化を運用する 型安全な設計と保守の要点
article2026年1月13日TypeScriptでHigher Kinded Typesを模倣する設計 ジェネリクスで関数型パターンを整理
article2026年1月13日Viteで画像とアセット管理をシンプルにする使い方 import運用と構成の考え方
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 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
