TypeScript で始めるモノレポ管理:プロジェクト分割と依存関係制御の実践

複数のプロジェクトを効率的に管理したい、コードの重複を減らしたい、チーム開発での作業効率を向上させたい。そんな課題を抱える開発者の皆さんにとって、モノレポ(Monorepo)は非常に魅力的な解決策です。特に TypeScript プロジェクトにおいて、モノレポ管理は開発効率を劇的に向上させる可能性を秘めています。
本記事では、TypeScript プロジェクトでのモノレポ管理について、基礎概念から実際の構築手順まで詳しく解説いたします。Yarn Workspaces を中心とした実践的なアプローチで、皆さんの開発体験を向上させるお手伝いをさせていただきますね。
背景
モノレポとは何か
モノレポとは、複数の関連するプロジェクトやパッケージを単一のリポジトリで管理する開発手法です。従来のマルチレポ(複数リポジトリ)とは対照的に、すべてのコードを一箇所に集約することで、様々なメリットを享受できます。
モノレポの基本構造例
typescriptmy-monorepo/
├── packages/
│ ├── shared-utils/ # 共通ユーティリティ
│ ├── ui-components/ # UIコンポーネント
│ ├── frontend-app/ # フロントエンドアプリ
│ └── backend-api/ # バックエンドAPI
├── package.json # ルート設定
├── yarn.lock # 依存関係ロック
└── tsconfig.json # TypeScript設定
従来の開発手法との違い
マルチレポ(従来手法)の特徴
項目 | マルチレポ | モノレポ |
---|---|---|
リポジトリ数 | プロジェクトごとに個別 | 単一リポジトリ |
依存関係管理 | npm/yarn link が必要 | ワークスペース内で自動解決 |
コード共有 | パッケージ公開が必要 | 直接参照可能 |
バージョン管理 | 個別に管理 | 一括管理可能 |
CI/CD 設定 | 各リポジトリで個別設定 | 統一された設定 |
従来のマルチレポでは、共通ライブラリの更新時に複数のリポジトリを個別に更新する必要がありました。これは非常に手間がかかる作業でしたね。
TypeScript プロジェクトでモノレポが重要な理由
TypeScript の特性を考えると、モノレポとの相性は抜群です。その理由をご説明いたします。
型安全性の恩恵
TypeScript の最大の魅力である型安全性を、プロジェクト間で共有できます。
typescript// packages/shared-types/src/user.ts
export interface User {
id: string;
name: string;
email: string;
}
// packages/frontend-app/src/components/UserProfile.tsx
import { User } from '@my-org/shared-types';
// 型情報が自動で共有される
const UserProfile: React.FC<{ user: User }> = ({
user,
}) => {
return <div>{user.name}</div>;
};
統一されたツールチェーン
TypeScript プロジェクトでは、ESLint、Prettier、Jest などのツールを統一することで、開発体験が格段に向上します。
課題
複数プロジェクト管理の困難さ
実際の開発現場では、以下のような課題に直面することが多いのではないでしょうか。
個別リポジトリ管理の煩雑さ
複数のリポジトリを個別に管理する際によく遭遇するエラーです:
bash# よくあるエラー:依存関係の不整合
npm ERR! peer dep missing: react@^18.0.0, required by @my-org/ui-components@1.2.0
npm ERR! peer dep missing: typescript@^4.9.0, required by @my-org/shared-utils@2.1.0
このようなエラーは、各プロジェクトで異なるバージョンの依存関係を使用している際に発生します。
コードの重複問題
同じような機能が複数のプロジェクトで実装されてしまうことがあります:
typescript// プロジェクトA での実装
export const formatDate = (date: Date): string => {
return date.toLocaleDateString('ja-JP');
};
// プロジェクトB でも同じような実装
export const dateFormatter = (inputDate: Date): string => {
return inputDate.toLocaleDateString('ja-JP');
};
依存関係の複雑化
バージョン管理の混乱
個別リポジトリでは、依存関係のバージョンが散らばってしまいがちです:
json// プロジェクトA の package.json
{
"dependencies": {
"react": "^17.0.2",
"typescript": "^4.5.0"
}
}
// プロジェクトB の package.json
{
"dependencies": {
"react": "^18.2.0",
"typescript": "^4.9.0"
}
}
この状況では、型定義の不整合やランタイムエラーが発生する可能性があります。
ビルド時間の増大
重複したビルドプロセス
各プロジェクトで個別にビルドを実行すると、以下のような無駄が発生します:
bash# 各プロジェクトで個別にビルド実行
cd project-a && yarn build # 5分
cd project-b && yarn build # 4分
cd project-c && yarn build # 3分
# 合計:12分
チーム開発での課題
知識の分散
プロジェクトが分散していると、チームメンバーが全体像を把握するのが困難になります。新しいメンバーが参加した際の学習コストも高くなってしまいますね。
解決策
Yarn Workspaces によるモノレポ構築
Yarn Workspaces は、モノレポ管理のための強力なツールです。設定も比較的簡単で、TypeScript プロジェクトとの相性も抜群です。
ルート package.json の設定
まず、プロジェクトのルートディレクトリに以下の設定を行います:
json{
"name": "my-typescript-monorepo",
"private": true,
"workspaces": ["packages/*"],
"scripts": {
"build": "yarn workspaces run build",
"test": "yarn workspaces run test",
"lint": "yarn workspaces run lint"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/node": "^18.0.0",
"eslint": "^8.0.0",
"prettier": "^2.8.0"
}
}
ワークスペースの初期化
ワークスペースを初期化する際は、以下のコマンドを実行します:
bash# プロジェクトルートで実行
yarn install
# 成功時の出力例
yarn install v1.22.19
info No lockfile found.
[1/4] 🔍 Resolving packages...
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
[4/4] 🔨 Building fresh packages...
✨ Done in 15.23s.
TypeScript 設定の最適化
階層化された tsconfig.json
モノレポでは、設定を階層化することで管理効率を向上させます:
json// ルートの tsconfig.json (基本設定)
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"@my-org/*": ["packages/*/src"]
}
},
"references": [
{ "path": "./packages/shared-utils" },
{ "path": "./packages/ui-components" },
{ "path": "./packages/frontend-app" }
]
}
パッケージ固有の設定
各パッケージでは、ルート設定を継承しつつ固有の設定を追加します:
json// packages/shared-utils/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*"],
"exclude": ["dist", "node_modules"]
}
依存関係制御の戦略
内部パッケージの参照設定
モノレポ内のパッケージ間で依存関係を設定する方法です:
json// packages/frontend-app/package.json
{
"name": "@my-org/frontend-app",
"dependencies": {
"@my-org/shared-utils": "workspace:*",
"@my-org/ui-components": "workspace:*",
"react": "^18.2.0"
}
}
workspace:*
を使用することで、ワークスペース内の最新バージョンを自動参照できます。
依存関係の一元管理
共通の依存関係はルートで管理し、各パッケージでは必要最小限の依存関係のみを定義します:
json// ルート package.json (開発依存関係を一元管理)
{
"devDependencies": {
"typescript": "^5.0.0",
"eslint": "^8.0.0",
"prettier": "^2.8.0",
"jest": "^29.0.0"
}
}
ビルドパフォーマンスの改善
増分ビルドの活用
TypeScript の Project References を活用することで、変更されたパッケージのみをビルドできます:
bash# 全体をビルド(初回のみ)
yarn tsc --build
# 増分ビルド(変更されたファイルのみ)
yarn tsc --build --incremental
並列ビルドの実装
複数のパッケージを並列でビルドすることで、全体のビルド時間を短縮できます:
json// package.json に並列ビルドスクリプトを追加
{
"scripts": {
"build:parallel": "yarn workspaces run build --parallel",
"build:changed": "yarn workspaces run build --since=origin/main"
}
}
具体例
プロジェクト構造の設計
実際のプロジェクト構造を詳しく見ていきましょう:
csharptypescript-monorepo/
├── packages/
│ ├── shared-utils/ # 共通ユーティリティ
│ │ ├── src/
│ │ │ ├── date.ts
│ │ │ ├── validation.ts
│ │ │ └── index.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── ui-components/ # UIコンポーネント
│ │ ├── src/
│ │ │ ├── Button/
│ │ │ ├── Input/
│ │ │ └── index.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── frontend-app/ # メインアプリケーション
│ │ ├── src/
│ │ │ ├── components/
│ │ │ ├── pages/
│ │ │ └── App.tsx
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── backend-api/ # バックエンドAPI
│ ├── src/
│ │ ├── controllers/
│ │ ├── models/
│ │ └── server.ts
│ ├── package.json
│ └── tsconfig.json
├── package.json # ルート設定
├── yarn.lock # 依存関係ロック
├── tsconfig.json # TypeScript基本設定
└── .eslintrc.js # ESLint設定
package.json 設定
ルート package.json の詳細設定
json{
"name": "typescript-monorepo",
"version": "1.0.0",
"private": true,
"workspaces": ["packages/*"],
"scripts": {
"build": "yarn tsc --build",
"build:watch": "yarn tsc --build --watch",
"clean": "yarn tsc --build --clean",
"test": "jest",
"test:watch": "jest --watch",
"lint": "eslint packages/*/src/**/*.ts",
"lint:fix": "eslint packages/*/src/**/*.ts --fix",
"format": "prettier --write packages/*/src/**/*.{ts,tsx}",
"dev:frontend": "yarn workspace @my-org/frontend-app dev",
"dev:backend": "yarn workspace @my-org/backend-api dev"
},
"devDependencies": {
"typescript": "^5.0.0",
"@types/node": "^18.0.0",
"eslint": "^8.40.0",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"prettier": "^2.8.8",
"jest": "^29.5.0",
"@types/jest": "^29.5.0",
"ts-jest": "^29.1.0"
}
}
個別パッケージの設定例
json// packages/shared-utils/package.json
{
"name": "@my-org/shared-utils",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"build:watch": "tsc --watch",
"test": "jest"
},
"dependencies": {
"date-fns": "^2.30.0"
}
}
tsconfig.json の階層化
プロジェクトリファレンスの活用
json// ルート tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020", "DOM"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"@my-org/shared-utils": ["packages/shared-utils/src"],
"@my-org/ui-components": [
"packages/ui-components/src"
]
},
"composite": true,
"declaration": true,
"declarationMap": true,
"incremental": true
},
"files": [],
"references": [
{ "path": "./packages/shared-utils" },
{ "path": "./packages/ui-components" },
{ "path": "./packages/frontend-app" },
{ "path": "./packages/backend-api" }
]
}
パッケージ固有の TypeScript 設定
json// packages/ui-components/tsconfig.json
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"jsx": "react-jsx",
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": [
"dist",
"node_modules",
"**/*.test.ts",
"**/*.test.tsx"
],
"references": [{ "path": "../shared-utils" }]
}
共通ライブラリの作成と活用
型安全な共通ユーティリティの実装
typescript// packages/shared-utils/src/validation.ts
/**
* バリデーションエラーの型定義
*/
export interface ValidationError {
field: string;
message: string;
code: string;
}
/**
* バリデーション結果の型定義
*/
export interface ValidationResult<T> {
isValid: boolean;
data?: T;
errors: ValidationError[];
}
/**
* メールアドレスの形式をチェックする関数
* @param email - チェック対象のメールアドレス
* @returns バリデーション結果
*/
export const validateEmail = (
email: string
): ValidationResult<string> => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email) {
return {
isValid: false,
errors: [
{
field: 'email',
message: 'メールアドレスは必須です',
code: 'REQUIRED',
},
],
};
}
if (!emailRegex.test(email)) {
return {
isValid: false,
errors: [
{
field: 'email',
message:
'正しいメールアドレス形式で入力してください',
code: 'INVALID_FORMAT',
},
],
};
}
return {
isValid: true,
data: email,
errors: [],
};
};
共通 UI コンポーネントの実装
typescript// packages/ui-components/src/Button/Button.tsx
import React from 'react';
/**
* ボタンコンポーネントのプロパティ型定義
*/
export interface ButtonProps {
/** ボタンのラベルテキスト */
children: React.ReactNode;
/** ボタンのバリアント(見た目の種類) */
variant?: 'primary' | 'secondary' | 'danger';
/** ボタンのサイズ */
size?: 'small' | 'medium' | 'large';
/** 無効化フラグ */
disabled?: boolean;
/** クリック時のイベントハンドラ */
onClick?: () => void;
}
/**
* 再利用可能なボタンコンポーネント
* 全プロジェクトで統一されたスタイルを提供
*/
export const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
size = 'medium',
disabled = false,
onClick,
}) => {
// スタイルクラスの生成(実際のプロジェクトではCSS-in-JSやCSSモジュールを使用)
const baseClasses = 'btn';
const variantClasses = `btn--${variant}`;
const sizeClasses = `btn--${size}`;
const disabledClasses = disabled ? 'btn--disabled' : '';
const className = [
baseClasses,
variantClasses,
sizeClasses,
disabledClasses,
]
.filter(Boolean)
.join(' ');
return (
<button
className={className}
disabled={disabled}
onClick={onClick}
type='button'
>
{children}
</button>
);
};
プロジェクト間での型共有
typescript// packages/shared-utils/src/types/api.ts
/**
* API共通レスポンス型
*/
export interface ApiResponse<T = any> {
success: boolean;
data?: T;
message?: string;
errors?: string[];
}
/**
* ユーザー情報の型定義
*/
export interface User {
id: string;
name: string;
email: string;
createdAt: string;
updatedAt: string;
}
/**
* ページネーション情報の型定義
*/
export interface Pagination {
page: number;
limit: number;
total: number;
totalPages: number;
}
/**
* ページネーション付きレスポンスの型定義
*/
export interface PaginatedResponse<T>
extends ApiResponse<T[]> {
pagination: Pagination;
}
CI/CD パイプラインの構築
GitHub Actions を使用した自動化
yaml# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
# 依存関係のインストールとキャッシュ
setup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
# リンターとフォーマッターの実行
lint:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- run: yarn install --frozen-lockfile
- run: yarn lint
- run: yarn format --check
# 型チェックの実行
typecheck:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- run: yarn install --frozen-lockfile
- run: yarn tsc --noEmit
# テストの実行
test:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- run: yarn install --frozen-lockfile
- run: yarn test --coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
# ビルドの実行
build:
needs: [lint, typecheck, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'yarn'
- run: yarn install --frozen-lockfile
- run: yarn build
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build-files
path: packages/*/dist
変更検知による効率的なビルド
モノレポでは、変更されたパッケージのみを対象にビルドすることで、CI/CD 時間を短縮できます:
bash# 変更されたファイルを検知してビルド
yarn workspaces run build --since=origin/main
# 特定のパッケージとその依存関係のみビルド
yarn workspace @my-org/frontend-app build
よくある CI/CD エラーと対処法
bash# エラー例1: ワークスペースの依存関係エラー
yarn install v1.22.19
[1/4] Resolving packages...
error Couldn't find package "@my-org/shared-utils@workspace:*" on the "npm" registry.
# 解決策: 依存関係の順序を正しく設定
# packages/frontend-app/package.json で正しく参照する
{
"dependencies": {
"@my-org/shared-utils": "workspace:*"
}
}
bash# エラー例2: TypeScript プロジェクトリファレンスエラー
error TS6053: File 'packages/shared-utils/tsconfig.json' not found.
# 解決策: プロジェクトリファレンスのパスを確認
# ルート tsconfig.json で正しいパスを指定
{
"references": [
{ "path": "./packages/shared-utils" }
]
}
まとめ
モノレポ導入のメリット・デメリット
メリット
項目 | 詳細 |
---|---|
コード共有の効率化 | 共通ライブラリを簡単に作成・利用可能 |
依存関係の一元管理 | バージョン不整合を防止 |
開発体験の向上 | 統一されたツールチェーンと設定 |
リファクタリングの容易さ | 影響範囲の把握と一括変更が可能 |
CI/CD の効率化 | 統一されたパイプラインで管理効率向上 |
デメリット
項目 | 対策 |
---|---|
リポジトリサイズの増大 | 適切なブランチ戦略とアーカイブ機能の活用 |
ビルド時間の増加 | 増分ビルドと並列処理の活用 |
権限管理の複雑化 | セキュリティポリシーの明確化 |
初期学習コスト | ドキュメント整備と段階的導入 |
導入時の注意点
段階的な移行戦略
モノレポへの移行は一度に行わず、段階的に進めることをおすすめします:
Phase 1: 基盤構築
- Yarn Workspaces の設定
- TypeScript 設定の統一
- 基本的な CI/CD 構築
Phase 2: 共通ライブラリの移行
- 共通ユーティリティの集約
- 型定義の共有
- UI コンポーネントの統一
Phase 3: アプリケーションの統合
- メインアプリケーションの移行
- 依存関係の最適化
- パフォーマンス改善
成功のポイント
- 明確なディレクトリ構造: チーム全体で理解しやすい構造を設計
- 適切な抽象化: 共通化しすぎず、適度な独立性を保つ
- 継続的な改善: 定期的な設定見直しとアップデート
- ドキュメント整備: 新メンバーでも理解できる詳細な手順書
TypeScript でのモノレポ管理は、初期の学習コストはあるものの、中長期的な開発効率向上には非常に有効な手法です。特にチーム開発における品質向上と保守性の向上は、投資した時間以上の価値を提供してくれるでしょう。
ぜひ皆さんのプロジェクトでも、段階的にモノレポ管理を導入してみてくださいね。最初は小さく始めて、徐々に範囲を拡大していく方法が成功の秘訣です。
関連リンク
- blog
うちのチーム、これやってない?アジャイル開発を腐らせる、ありがちなアンチパターン 10 選と処方箋
- blog
CD パイプラインを構築して、開発チームを「リリース疲れ」から解放しよう
- blog
見積もりが全然当たらないあなたへ。プランニングポーカーで楽しく、納得感のある見積もりをするコツ
- blog
「QA は最後の砦」という幻想を捨てる。開発プロセスに QA を組み込み、手戻りをなくす方法
- blog
ドキュメントは「悪」じゃない。アジャイル開発で「ちょうどいい」ドキュメントを見つけるための思考法
- blog
「アジャイルコーチ」って何する人?チームを最強にする影の立役者の役割と、あなたがコーチになるための道筋
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
- review
人類はなぜ地球を支配できた?『サピエンス全史 上巻』ユヴァル・ノア・ハラリが解き明かす驚愕の真実
- review
え?世界はこんなに良くなってた!『FACTFULNESS』ハンス・ロスリングが暴く 10 の思い込みの正体
- review
瞬時に答えが出る脳に変身!『ゼロ秒思考』赤羽雄二が贈る思考力爆上げトレーニング
- review
関西弁のゾウに人生変えられた!『夢をかなえるゾウ 1』水野敬也が教えてくれた成功の本質
- review
「なぜ私の考えは浅いのか?」の答えがここに『「具体 ⇄ 抽象」トレーニング』細谷功