T-CREATOR

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

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: 基盤構築

  1. Yarn Workspaces の設定
  2. TypeScript 設定の統一
  3. 基本的な CI/CD 構築

Phase 2: 共通ライブラリの移行

  1. 共通ユーティリティの集約
  2. 型定義の共有
  3. UI コンポーネントの統一

Phase 3: アプリケーションの統合

  1. メインアプリケーションの移行
  2. 依存関係の最適化
  3. パフォーマンス改善

成功のポイント

  1. 明確なディレクトリ構造: チーム全体で理解しやすい構造を設計
  2. 適切な抽象化: 共通化しすぎず、適度な独立性を保つ
  3. 継続的な改善: 定期的な設定見直しとアップデート
  4. ドキュメント整備: 新メンバーでも理解できる詳細な手順書

TypeScript でのモノレポ管理は、初期の学習コストはあるものの、中長期的な開発効率向上には非常に有効な手法です。特にチーム開発における品質向上と保守性の向上は、投資した時間以上の価値を提供してくれるでしょう。

ぜひ皆さんのプロジェクトでも、段階的にモノレポ管理を導入してみてくださいね。最初は小さく始めて、徐々に範囲を拡大していく方法が成功の秘訣です。

関連リンク