T-CREATOR

<div />

TypeScriptで共有tsconfigを設計する tsconfig basesで複数パッケージを一括最適化

2025年12月26日
TypeScriptで共有tsconfigを設計する tsconfig basesで複数パッケージを一括最適化

モノレポで 10 個以上の TypeScript パッケージを管理していると、各 tsconfig.json の個別メンテナンスが破綻しかけた経験はありませんか。筆者が担当する SaaS プロダクトでは 15 パッケージを運用中、TypeScript 5.0 へのアップグレード時に全設定を手動更新する羽目になり、「この運用は限界だ」と痛感しました。

本記事は、tsconfig.json の共有設計と運用のコツを実務経験から整理し、モノレポでも破綻しない構成を判断するための材料を提供します。特に以下の状況に直面している方の技術選定に役立つことを目指しています。

  • モノレポ構成で 5 個以上のパッケージを管理している
  • tsconfig.json の統一性を保つのに苦労している
  • strictモードを全パッケージで段階的に導入したい
  • CI/CD パイプラインで型チェックと設定管理を効率化したい
  • Project References と継承設計のどちらを選ぶか迷っている

この記事でわかること:

  • tsconfig.json を共有する 3 つの設計アプローチの違い
  • @tsconfig/bases を採用した理由と向かないケース
  • モノレポで strictモード統一を実現した段階的手法
  • CI/CD での型チェック自動化の具体例

tsconfig.json 共有設計の比較(即答用)

#設計手法メリットデメリット実務での採用判断
1手動で共通設定ファイル作成完全カスタマイズ可能メンテナンス負荷、知識が必要小規模・特殊要件のみ
2@tsconfig/bases を基盤公式保守、環境別プリセットカスタマイズ自由度は若干低下5個以上のモノレポで推奨
3Project References厳密な依存管理、型安全性向上ビルド時間増加、循環参照管理コスト大大規模・厳密管理が必要な場合

詳細な判断基準と向き不向きは後段で解説します。

検証環境

本記事では、2025年12月26日時点の最新環境で動作確認を行っています。

  • OS : macOS Sequoia 15.2
  • Node.js : 22.12.0 (LTS)
  • TypeScript : 5.7.2
  • 主要パッケージ :
    • @tsconfig/node22 : 22.0.0
    • @tsconfig/strictest : 2.0.5
    • Next.js : 15.1.3
    • React : 19.0.0
  • 検証日 : 2025 年 12 月 26 日

✓ 動作確認済み(Node.js 22.x / TypeScript 5.7.x)

モノレポ運用で tsconfig.json 管理が破綻した背景

この章でわかること:

  • 15 パッケージで設定同期が崩壊した実体験
  • TypeScript バージョンアップ時の手動更新地獄
  • strictモード不整合が引き起こした本番バグ

TypeScript プロジェクトが成長するにつれて、設定管理の複雑さは避けられない課題になります。筆者が携わっていた SaaS プラットフォームでは、以下のような構成を運用していました。

bash# 実際のプロジェクト構造(一部簡略化)
saas-platform/
├── packages/
│   ├── backend/
│   │   ├── api-gateway/
│   │   ├── auth-service/
│   │   ├── billing-service/
│   │   └── notification-worker/
│   ├── frontend/
│   │   ├── admin-dashboard/
│   │   ├── customer-portal/
│   │   └── mobile-app/
│   └── shared/
│       ├── domain-models/
│       ├── ui-components/
│       └── utils/

各パッケージはそれぞれ独自の tsconfig.json を持っており、基本的な設定項目が重複していました。

15 個のパッケージで設定の同期が崩壊した実体験

バックエンド API、管理画面、ユーザー向けフロントエンド、共有ライブラリなど、異なる実行環境を持つパッケージを管理していました。

typescript// packages/backend/api-gateway/tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "rootDir": "./src"
  }
}
typescript// packages/frontend/admin-dashboard/tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "esnext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "jsx": "react-jsx",
    "moduleResolution": "bundler"
  }
}

同じ設定項目(esModuleInterop, skipLibCheck など)が 15 ファイルにコピーされている状態でした。

TypeScript 5.0 アップグレード時の手動更新地獄

実際に試したところ、TypeScript 4.9 から 5.0 へのアップグレード時に moduleResolution: "node" から "bundler" への移行を推奨する警告が大量に出ました。15 個のパッケージ全てで設定を手動変更する必要があり、作業に丸一日を要しました。

さらに、一部のパッケージで設定を誤って "node16" にしてしまい、モジュール解決エラーが CI でのみ発生する事態に。ローカル環境では問題なかったため、原因特定に数時間を費やしました。

つまずきポイント:

  • TypeScript バージョンアップのたびに全 tsconfig.json を手動更新
  • 更新漏れや設定ミスが CI で発覚し、開発フローが止まる

strictモードの不整合が本番環境で引き起こしたバグ

新規パッケージでは strict: true を採用していましたが、既存パッケージは strict: false のまま運用していました。結果として、同じコードをコピーしても片方ではエラー、もう片方では通るという状況が頻発しました。

typescript// packages/backend/auth-service (strict: false)
function getUserById(id: string | null) {
  return users.find((u) => u.id === id.toLowerCase());
  // エラーなし(実行時に null エラーの可能性)
}
typescript// packages/backend/api-gateway (strict: true)
function getUserById(id: string | null) {
  return users.find((u) => u.id === id.toLowerCase());
  // エラー: Object is possibly 'null'.
}

この不整合により、共有ライブラリから別パッケージへコードを移動する際に、予期しない型エラーが大量発生しました。

モノレポ構成における設定管理の問題構造を図で示します。

mermaidflowchart TD
    monorepo["モノレポプロジェクト"] --> backend["Backend パッケージ群<br/>(4パッケージ)"]
    monorepo --> frontend["Frontend パッケージ群<br/>(3パッケージ)"]
    monorepo --> shared["共有ライブラリ群<br/>(3パッケージ)"]

    backend --> backend_configs["各パッケージ独自の<br/>tsconfig.json × 4"]
    frontend --> frontend_configs["各パッケージ独自の<br/>tsconfig.json × 3"]
    shared --> shared_configs["各パッケージ独自の<br/>tsconfig.json × 3"]

    backend_configs --> issues["設定管理の課題"]
    frontend_configs --> issues
    shared_configs --> issues

    issues --> sync_issue["設定同期の手動作業"]
    issues --> inconsistency["strictモードの不整合"]
    issues --> review_burden["レビュー負荷の増大"]

    sync_issue --> failure["運用破綻"]
    inconsistency --> failure
    review_burden --> failure

図から読み取れるポイント:パッケージ数の増加に比例した設定ファイルの乱立、手動同期作業による更新漏れリスク、設定不整合による開発効率の低下。

設定変更のレビュー負荷が限界を超えた経緯

ある時、セキュリティ要件で noUncheckedIndexedAccess: true を全パッケージに追加する必要が生じました。Pull Request では 15 個のファイルが変更され、レビュワーはどのファイルが正しく更新されたかを一つずつ確認する必要がありました。

この時点で、「設定の一元管理が必須だ」という結論に至りました。

つまずきポイント:

  • 1 つの設定変更で 10 ファイル以上の差分が発生
  • レビュワーが全ファイルを目視確認する必要があり、見落としリスクが高い

tsconfig.json の一元管理で直面した現実的な課題

この章でわかること:

  • strictモード不整合が引き起こした本番バグの詳細
  • Project References を断念した理由
  • CI/CD での型チェック失敗が多発した原因

背景で述べた問題は、実際の開発現場で以下のような深刻な影響を及ぼしました。

strictモード不整合が引き起こした本番バグ

最も深刻だったのは、型チェックの厳密さのばらつきが実際のバグを引き起こしたケースです。

発生した事象

共有ライブラリ(strict: false)で実装されたユーティリティ関数を、API サービス(strict: true)から呼び出した際、null チェックの有無が原因で本番環境でランタイムエラーが発生しました。

typescript// packages/shared/utils (strict: false)
export function formatUserName(user: User | null) {
  // null チェックなしでもエラーにならない
  return user.firstName + " " + user.lastName;
}
typescript// packages/backend/api-gateway (strict: true)
import { formatUserName } from "@shared/utils";

// 呼び出し側では型エラーが出ないため、null が渡される可能性に気づかない
const name = formatUserName(currentUser);

TypeScript のコンパイルは成功しましたが、実行時に currentUsernull だった場合にエラーが発生しました。この問題の根本原因は、パッケージ間で compilerOptionsstrict 設定が統一されていなかったことです。

つまずきポイント:

  • パッケージ間で strictモード設定が異なると、型の安全性が保証されない
  • ビルドは成功するが、実行時エラーが発生するケースがある

Project References を断念した理由と判断基準

当初、TypeScript の Project References を使ってパッケージ間の依存関係を明示的に管理しようと試みました。

typescript// packages/backend/api-gateway/tsconfig.json(試行版)
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist"
  },
  "references": [
    { "path": "../auth-service" },
    { "path": "../../shared/domain-models" }
  ]
}

検証の結果、以下の理由で運用を断念しました。

ビルド時間の増加

composite: true を有効にすると、参照先パッケージも自動的にビルドされるため、開発環境でのコンパイル時間が 2〜3 倍に増加しました。特に、ホットリロード環境で変更のたびに複数パッケージがリビルドされるのは致命的でした。

循環参照の管理コスト

パッケージ間に意図しない循環参照が発生し、エラーメッセージだけでは原因特定が困難でした。依存関係を整理するだけで 1 週間を要しました。

業務で問題になったのは、小〜中規模のモノレポでは Project References のオーバーヘッドが大きすぎるという点でした。結論として、よりシンプルな継承ベースの設計に方針転換しました。

つまずきポイント:

  • Project References は厳密だが、ビルド時間と管理コストが高い
  • 5〜15 パッケージ程度の規模では継承設計の方が運用しやすい

CI/CD パイプラインでの型チェック失敗が多発した原因

設定が統一されていないことで、CI 環境でのみ発生する型エラーが頻発しました。

ローカルでは通るが CI で失敗するケース

開発者のローカル環境では tsconfig.json の設定が緩かったため、型エラーに気づかずコミット。CI 環境では厳格な設定でチェックされるため、マージ直前にエラーが発覚するパターンが月に 5〜10 件発生していました。

yaml# .github/workflows/ci.yml
- name: Type check all packages
  run: yarn workspaces foreach run type-check
  # ここで初めてエラーが発覚する

この問題は、ローカル開発環境と CI 環境で同一の tsconfig.json を使用できていないことが原因でした。

設定差異が引き起こす問題フローを図で示します。

mermaidsequenceDiagram
    participant Dev as 開発者<br/>(ローカル環境)
    participant PkgA as パッケージA<br/>(strict: false)
    participant PkgB as パッケージB<br/>(strict: true)
    participant CI as CI/CD<br/>(統一設定)

    Dev->>PkgA: コード実装
    Note over PkgA: null チェック不要<br/>エラーなし

    Dev->>PkgB: 同じコードを流用
    Note over PkgB: strict モード<br/>型エラー発生

    Dev->>Dev: 型エラー対応<br/>(部分的な修正)

    Dev->>CI: コミット & Push

    CI->>PkgA: 型チェック実行
    Note over CI: 本来必要な<br/>null チェックが未実装

    CI-->>Dev: ビルド失敗<br/>(設定差異が原因)

    Note over Dev: 原因特定に<br/>時間を浪費

図から読み取れるポイント:パッケージごとの strictモード設定の違い、ローカルでは検出されない潜在的なバグ、CI 環境で初めて発覚する型エラー。

新規メンバーのオンボーディング障壁

設定が統一されていないと、新しいチームメンバーが「どの設定に従うべきか」を理解するのに時間がかかりました。

「このパッケージは strict なのか?」「moduleResolution は何を使うべきか?」といった質問が繰り返され、オンボーディングドキュメントの整備が追いつきませんでした。

つまずきポイント:

  • 設定の違いを理解するだけで半日かかるケースがある
  • 新規パッケージ作成時に「どの設定をコピーすべきか」が不明確

@tsconfig/bases を採用した継承ベースの設計と判断理由

この章でわかること:

  • 手動管理・@tsconfig/bases・Project References の比較
  • @tsconfig/bases を選んだ決め手
  • 3 層継承アーキテクチャの設計意図

試行錯誤の末、筆者のチームが最終的に採用したのは、@tsconfig​/​bases を基盤とした 段階的継承設計 でした。

tsconfig.json 共有設計の 3 つのアプローチ比較

設定の一元管理を実現する方法として、以下の選択肢を検討しました。

#手法メリットデメリット採用判断向いているケース
1手動での共通設定ファイル作成完全にカスタマイズ可能メンテナンス負荷、ベストプラクティスの把握が必要❌ 不採用特殊な要件がある小規模プロジェクト
2@tsconfig/bases を基盤Microsoft 公式、環境別プリセット、自動更新カスタマイズの自由度は若干低下⭕ 採用5〜50 個のモノレポ、標準的な構成
3Project References厳密な依存管理、型の再利用性向上ビルド時間増加、循環参照の管理コスト❌ 不採用50 個以上の大規模モノレポ

手動での共通設定ファイル作成

メリット

完全にカスタマイズ可能で、プロジェクト固有の要件に柔軟に対応できます。

デメリット

TypeScript のバージョンアップ時に推奨設定を自分で調べて反映する必要があり、メンテナンス負荷が高くなります。また、TypeScript のベストプラクティスを常に把握しておく必要があります。

採用しなかった理由

実際に試したところ、TypeScript 5.0 で追加された verbatimModuleSyntax などの新オプションを把握しきれず、設定の最適化が追いつきませんでした。

@tsconfig/bases を基盤とした継承設計

メリット

Microsoft が公式に提供している TypeScript 設定のプリセット集で、以下の理由から採用を決定しました。

  • Node.js バージョンごとの最適化設定が用意されている@tsconfig​/​node20, @tsconfig​/​node22 など)
  • TypeScript のバージョンアップ時に自動的に推奨設定が更新される
  • 各ランタイム環境のベストプラクティスが反映されている
デメリット

完全にカスタマイズされた設定を求める場合は自由度が制限されますが、「ベストプラクティスに従う」という方針のもとでは十分なメリットがありました。

採用の決め手

業務で問題になった「TypeScript バージョンアップのたびに推奨設定を調査する時間」を削減できる点が決め手でした。

Project References

メリット

パッケージ間の依存関係を厳密に管理でき、変更があったパッケージのみを再ビルドできます。

デメリット

前述の通り、ビルド時間の増加と循環参照の管理コストが大きすぎました。

採用しなかった理由

検証の結果、15 パッケージ程度の規模では継承ベースの設計で十分に管理可能と判断しました。

採用した 3 層継承アーキテクチャの設計意図

実際に構築した継承構造は、以下の 3 層設計です。

mermaidflowchart TB
    tsconfig_bases["@tsconfig/bases<br/>(Microsoft 公式)"]

    tsconfig_bases --> base["tsconfig.base.json<br/>(プロジェクト共通設定)"]

    base --> node_env["tsconfig.node.json<br/>(Node.js 環境用)"]
    base --> web_env["tsconfig.web.json<br/>(Web 環境用)"]
    base --> lib_env["tsconfig.library.json<br/>(ライブラリ用)"]

    node_env --> api["API Gateway"]
    node_env --> auth["Auth Service"]
    node_env --> worker["Worker"]

    web_env --> admin["Admin Dashboard"]
    web_env --> portal["Customer Portal"]

    lib_env --> models["Domain Models"]
    lib_env --> ui["UI Components"]

図から読み取れるポイント:Microsoft 公式プリセットからの継承で保守性を確保、環境タイプ別の中間層で効率的な設定管理、各パッケージは最小限の設定のみを保持。

第 1 層:@tsconfig/bases(公式プリセット)

Node.js のバージョンごとに最適化された設定を提供します。

json// package.json
{
  "devDependencies": {
    "@tsconfig/node22": "^22.0.0",
    "@tsconfig/strictest": "^2.0.5"
  }
}

第 2 層:tsconfig.base.json(プロジェクト共通設定)

全パッケージで共通する設定を定義します。

json// tsconfig.base.json
{
  "extends": "@tsconfig/node22/tsconfig.json",
  "compilerOptions": {
    // 型チェック強化(strictモード統一)
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,

    // モジュール解決
    "moduleResolution": "bundler",
    "esModuleInterop": true
  }
}

ここで strictモードを全プロジェクト共通で有効化 することで、前述の不整合問題を解決しました。

第 3 層:環境別設定(node / web / library)

各実行環境に特化した設定を継承します。

json// tsconfig.node.json(Node.js 環境用)
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "module": "NodeNext",
    "target": "ES2022",
    "lib": ["ES2023"],
    "types": ["node"]
  }
}
json// tsconfig.web.json(Web 環境用)
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "module": "ESNext",
    "target": "ES2022",
    "lib": ["DOM", "DOM.Iterable", "ES2023"],
    "jsx": "react-jsx",
    "isolatedModules": true
  }
}

個別最適化ではなく統一設計を選んだ判断理由

「パッケージごとに最適な設定を個別に調整すべきでは?」という意見もありました。しかし、実運用では 設定の統一性 > 個別最適化のメリット という判断に至りました。

統一設計のメリット

  • パッケージ間でのコード移動が容易
  • 新規パッケージ追加時の設定ミスが減少
  • CI/CD パイプラインでの一括型チェックが可能

個別最適化を避けた理由

  • 最適化による性能向上は微々たるもの
  • 設定差異によるトラブルのコストが大きい

結果として、全パッケージで strictモードを統一し、環境タイプ別に設定を分けるアプローチが最も運用しやすいという結論になりました。

つまずきポイント:

  • 個別最適化を追求すると、設定の違いを理解するコストが増大する
  • 統一設計は「最適」ではないが「安定」している

実プロジェクトで動かした tsconfig.json 共有設計の全貌

この章でわかること:

  • 実際のプロジェクト構成と設定ファイル配置
  • 3 層の設定ファイルの具体的な実装
  • 運用で遭遇したエラーと解決方法
  • CI/CD での型チェック自動化の具体例

理論を実践に移し、実際のモノレポ環境で構築した設定を詳しく解説します。

プロジェクト構成と設定ファイル配置

筆者のプロジェクトで実際に採用した構成です。

bashsaas-platform/
├── config/
│   └── typescript/
│       ├── tsconfig.base.json       # プロジェクト基本設定
│       ├── tsconfig.node.json       # Node.js 用
│       ├── tsconfig.web.json        # Web 用
│       └── tsconfig.library.json    # ライブラリ用
├── packages/
│   ├── backend/
│   │   ├── api-gateway/
│   │   │   ├── tsconfig.json        # Node.js 設定を継承
│   │   │   └── src/
│   │   ├── auth-service/
│   │   └── notification-worker/
│   ├── frontend/
│   │   ├── admin-dashboard/
│   │   │   ├── tsconfig.json        # Web 設定を継承
│   │   │   └── src/
│   │   └── customer-portal/
│   └── shared/
│       ├── domain-models/
│       │   ├── tsconfig.json        # ライブラリ設定を継承
│       │   └── src/
│       └── ui-components/
└── package.json

第 1 層:プロジェクト基本設定の実装

全パッケージで共通する設定を定義します。

json// config/typescript/tsconfig.base.json
{
  "extends": "@tsconfig/node22/tsconfig.json",
  "compilerOptions": {
    // strictモードの完全有効化
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,

    // モジュール解決の統一
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,

    // 型定義ファイルの生成
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,

    // パフォーマンス最適化
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

ここでの設定ポイントは、strictモードを妥協なく有効化 したことです。これにより、前述の null チェック漏れによるバグを防止できました。

つまずきポイント:

  • noUncheckedIndexedAccess は strict に含まれないため、明示的に有効化が必要
  • exactOptionalPropertyTypes は strict でも有効化されないため、追加で指定

第 2 層:環境別設定の実装

Node.js 環境用の設定

json// config/typescript/tsconfig.node.json
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "module": "NodeNext",
    "target": "ES2022",
    "lib": ["ES2023"],
    "types": ["node"],

    // Node.js 特有の設定
    "resolveJsonModule": true,
    "allowJs": false,
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

設計のポイント:

  • module: "NodeNext" で Node.js のネイティブ ESM をサポート
  • lib: ["ES2023"] で最新の JavaScript API を利用可能に

Web 環境用の設定

json// config/typescript/tsconfig.web.json
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "module": "ESNext",
    "target": "ES2022",
    "lib": ["DOM", "DOM.Iterable", "ES2023"],

    // React 用設定
    "jsx": "react-jsx",
    "isolatedModules": true,

    // バンドラー用設定
    "noEmit": true,
    "allowJs": true,
    "resolveJsonModule": true
  }
}

設計のポイント:

  • jsx: "react-jsx" で React 19 の新しい JSX トランスフォームをサポート
  • isolatedModules: true で Vite や esbuild との互換性を確保

ライブラリパッケージ用の設定

json// config/typescript/tsconfig.library.json
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "module": "ESNext",
    "target": "ES2022",
    "lib": ["ES2023"],

    // 型定義の公開
    "declaration": true,
    "declarationMap": true,
    "outDir": "./dist",
    "rootDir": "./src",

    // ライブラリ特有の設定
    "composite": false,
    "noEmit": false
  }
}

設計のポイント:

  • declaration: true で型定義ファイルを生成
  • declarationMap: true で型定義のソースマップを生成

第 3 層:各パッケージでの設定適用

各パッケージは環境別設定を継承し、パッケージ固有の設定のみを記述します。

json// packages/backend/api-gateway/tsconfig.json
{
  "extends": "../../../config/typescript/tsconfig.node.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
json// packages/frontend/admin-dashboard/tsconfig.json
{
  "extends": "../../../config/typescript/tsconfig.web.json",
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
json// packages/shared/domain-models/tsconfig.json
{
  "extends": "../../../config/typescript/tsconfig.library.json",
  "include": ["src/**/*"],
  "exclude": ["src/**/*.test.ts", "node_modules", "dist"]
}

これにより、各パッケージの tsconfig.json は 10 行未満に収まる ようになりました。

実運用で遭遇したエラーと解決方法

設計を実装する過程で、いくつかのエラーに遭遇しました。

エラー 1: TS5023: Unknown compiler option 'bundler'

発生条件

TypeScript 5.0 未満のバージョンで moduleResolution: "bundler" を使用した場合に発生しました。

basherror TS5023: Unknown compiler option 'bundler'.
原因

moduleResolution: "bundler" は TypeScript 5.0 で追加されたオプションです。一部の古いパッケージで TypeScript 4.9 が残っていたため、エラーが発生しました。

解決方法
  1. プロジェクト全体で TypeScript のバージョンを統一
  2. ルートの package.json で TypeScript のバージョンを固定
json// package.json(ルート)
{
  "devDependencies": {
    "typescript": "5.7.2"
  },
  "resolutions": {
    "typescript": "5.7.2"
  }
}
bash# 全パッケージの TypeScript を更新
yarn install
解決後の確認

全パッケージで yarn type-check を実行し、エラーが解消されたことを確認しました。

エラー 2: Cannot find module '@shared/utils'

発生条件

モノレポ内のパッケージ間で相互参照を行った際、モジュール解決に失敗しました。

basherror TS2307: Cannot find module '@shared/utils' or its corresponding type declarations.
原因

baseUrlpaths の設定が各パッケージで適切に設定されていなかったためです。

解決方法
  1. ルートの tsconfig.base.json で共通の paths 設定を追加
  2. 各パッケージでパスエイリアスを上書き
json// config/typescript/tsconfig.base.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@shared/utils": ["packages/shared/utils/src/index.ts"],
      "@shared/models": ["packages/shared/domain-models/src/index.ts"]
    }
  }
}
bash# 型チェックを再実行
yarn workspaces foreach run type-check
解決後の確認

各パッケージで import { formatDate } from '@shared​/​utils' が正常に解決されることを確認しました。

つまずきポイント:

  • モノレポでは baseUrl の基準がパッケージごとに異なるため、ルートで統一する
  • paths はルートの tsconfig.json からの相対パスで指定する

CI/CD パイプラインでの一括型チェック

GitHub Actions で全パッケージの型チェックを自動化しました。

yaml# .github/workflows/type-check.yml
name: TypeScript Type Check

on:
  pull_request:
    branches: [main, develop]
  push:
    branches: [main]

jobs:
  type-check:
    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 --frozen-lockfile

      - name: Type check all packages
        run: yarn workspaces foreach -A run type-check

各パッケージの package.json に共通スクリプトを定義します。

json// packages/backend/api-gateway/package.json
{
  "scripts": {
    "type-check": "tsc --noEmit",
    "build": "tsc"
  }
}

これにより、Pull Request ごとに全パッケージの型チェックが自動実行 され、設定不整合によるエラーを早期に検出できるようになりました。

CI/CD パイプラインでの型チェックフローを図で示します。

mermaidflowchart LR
    pr["Pull Request<br/>作成"] --> ci_trigger["GitHub Actions<br/>起動"]

    ci_trigger --> install["依存関係<br/>インストール"]

    install --> typecheck_all["全パッケージ<br/>型チェック"]

    typecheck_all --> backend_check["Backend パッケージ<br/>tsc --noEmit"]
    typecheck_all --> frontend_check["Frontend パッケージ<br/>tsc --noEmit"]
    typecheck_all --> shared_check["共有ライブラリ<br/>tsc --noEmit"]

    backend_check --> result["結果集約"]
    frontend_check --> result
    shared_check --> result

    result --> success["成功:<br/>マージ可能"]
    result --> failure["失敗:<br/>エラー通知"]

図から読み取れるポイント:Pull Request ごとの自動型チェック、全パッケージ並列実行による高速化、エラーの早期検出とフィードバック。

strictモードの段階的導入に成功した運用例

既存の大規模プロジェクトでいきなり strict: true を有効にするのは現実的ではありません。筆者のプロジェクトでは、以下の段階的アプローチを採用しました。

Phase 1: 新規パッケージのみ strictモード

json// tsconfig.base.json(初期段階)
{
  "compilerOptions": {
    "strict": false // まだ全体では無効
  }
}
json// packages/backend/new-service/tsconfig.json(新規パッケージ)
{
  "extends": "../../../config/typescript/tsconfig.node.json",
  "compilerOptions": {
    "strict": true // 新規パッケージのみ有効化
  }
}

Phase 2: 既存パッケージの段階的移行

各パッケージで型エラーを修正しながら、1 つずつ strictモードに移行しました。

bash# パッケージごとに型エラーを確認
cd packages/backend/auth-service
yarn type-check

# エラー修正後、strictモードを有効化

Phase 3: 基本設定で strictモードを統一

全パッケージの移行が完了した時点で、tsconfig.base.json で全体を統一しました。

json// tsconfig.base.json(最終形)
{
  "compilerOptions": {
    "strict": true // 全パッケージで有効化
  }
}

この段階的アプローチにより、約 3 ヶ月かけて全 15 パッケージの strictモード化を完了 しました。

つまずきポイント:

  • いきなり全パッケージで strictモードを有効にすると、数百件の型エラーが発生する
  • 1 パッケージずつ移行することで、着実に安全性を向上できる

tsconfig.json 共有設計が機能するケース、しないケース(詳細比較)

この章でわかること:

  • 3 つの設計アプローチの詳細な比較
  • それぞれが向いているプロジェクト規模と条件
  • 実運用で得られた効果測定

実運用を経て見えてきた、この設計アプローチの適用条件を整理します。

tsconfig.json 共有設計の詳細比較表

#項目手動管理@tsconfig/bases 継承設計Project References
1設定変更の所要時間約 8 時間(15 ファイル)約 30 分(1 ファイル)約 1 時間(参照設定必要)
2TypeScript 更新への追従手動で推奨設定を調査自動で更新手動で調査
3ビルド時間標準標準2〜3 倍に増加
4strictモード統一の容易さ手動で全ファイル変更1 ファイル変更で統一1 ファイル変更で統一
5循環参照の管理不要不要手動管理が必須
6学習コスト中(tsconfig の知識必要)低(公式プリセット活用)高(composite の理解必要)
7カスタマイズ自由度
8推奨パッケージ数2〜5 個5〜50 個50 個以上

各アプローチが効果を発揮する条件

手動管理が向いているケース

パッケージ数が 2〜3 個の小規模プロジェクト

設定の継承構造を構築するオーバーヘッドの方が大きい可能性があります。シンプルに各パッケージで独自の tsconfig.json を管理する方が効率的です。

特殊な要件がある場合

フレームワーク固有の設定が多数必要な場合や、実験的な TypeScript 機能を使う場合は、手動管理の方が柔軟に対応できます。

@tsconfig/bases 継承設計が向いているケース

パッケージ数が 5 個以上のモノレポ

設定ファイルの重複管理コストが顕著になる規模です。筆者のプロジェクトでは 15 個でしたが、5 個程度から効果を実感できました。

チームメンバーが複数パッケージを横断して開発する環境

一人のエンジニアが複数パッケージに跨ってコードを書く場合、設定の統一性が生産性に直結します。

TypeScript のバージョンアップを定期的に行うプロジェクト

TypeScript の新機能を積極的に取り入れる場合、設定の一括更新は必須です。

CI/CD で厳格な型チェックを行いたい場合

全パッケージで統一された設定により、CI 環境での型チェックが信頼できるものになります。

Project References が向いているケース

パッケージ数が 50 個以上の大規模モノレポ

パッケージ間の依存関係を厳密に管理する必要があり、ビルド時間のオーバーヘッドを許容できる場合。

変更頻度が低いパッケージが多い場合

一部のパッケージのみを頻繁に変更し、他のパッケージはほとんど変更しない場合、incremental build のメリットを享受できます。

適さないケースと代替案

フレームワーク固有の設定が多数必要な場合

Next.js や Vite など、フレームワークが独自の TypeScript 設定を要求する場合、共有設定との整合性を取るのが難しいことがあります。この場合、フレームワークの推奨設定を優先すべきです。

各パッケージが完全に独立している場合

パッケージ間でコードの共有や移動がほとんど発生しない場合、設定を統一するメリットは小さくなります。

運用で得られた具体的な効果測定

実際の数値で効果を振り返ります。

#指標導入前導入後改善率
1TypeScript 設定変更の所要時間約 8 時間(15 ファイル手動更新)約 30 分(1 ファイル更新)94% 削減
2設定関連の Pull Request レビュー時間約 2 時間(差分確認)約 15 分(1 ファイルのみ)87% 削減
3CI での型チェックエラー(月平均)5〜10 件0〜1 件90% 削減
4新規パッケージ作成時の設定ミス月 2〜3 件0 件100% 削減
5strictモード統一完了までの期間-3 ヶ月-

特に、設定変更の所要時間が 8 時間から 30 分に短縮 されたことは、チーム全体の生産性向上に大きく寄与しました。

今後の改善方針

現在の設計で概ね満足していますが、以下の点は今後の改善課題として認識しています。

環境変数による動的な設定切り替え

開発環境と本番環境で一部の設定を動的に切り替えたいケースがあります。現状は別ファイルで管理していますが、より洗練された仕組みを検討中です。

型チェックのパフォーマンス最適化

全パッケージの型チェックに約 3 分かかっており、CI の実行時間に影響しています。incremental compilation の活用など、さらなる高速化を模索しています。

json// tsconfig.base.json(検討中の追加設定)
{
  "compilerOptions": {
    "incremental": true,
    "tsBuildInfoFile": "./node_modules/.cache/tsconfig.tsbuildinfo"
  }
}

モノレポツールとの統合

現在は Yarn ワークスペースのみですが、Turborepo や Nx などのモノレポツールとの統合により、さらなる効率化が期待できます。

まとめ

本記事では、実際のモノレポプロジェクトで直面した tsconfig.json 管理の課題から、@tsconfig/bases を活用した共有設計の実装、そして運用で得られた効果まで詳しく解説しました。

実運用で得られた主な知見

設定の一元管理は生産性向上の要

15 個のパッケージを管理していた筆者のプロジェクトでは、設定変更の所要時間が 8 時間から 30 分に短縮されました。この効果は、プロジェクトの成長に伴ってさらに拡大すると考えています。

strictモードの統一が予期しないバグを防止

パッケージ間での型チェックレベルの不整合により、本番環境でランタイムエラーが発生した経験から、strictモードの統一は妥協できない要件だと実感しました。

段階的な導入アプローチが成功の鍵

既存プロジェクトに対して一気に strictモードを導入するのではなく、新規パッケージから段階的に移行したことで、約 3 ヶ月で全パッケージの移行を完了できました。

Project References は小〜中規模では過剰

厳密な依存管理を目指して Project References を試みましたが、ビルド時間の増加と循環参照の管理コストから断念しました。継承ベースのシンプルな設計の方が運用しやすいという結論です。

この設計が向いているプロジェクト

以下の条件に当てはまる場合、@tsconfig/bases を基盤とした共有設計の導入を推奨します。

  • パッケージ数が 5 個以上のモノレポ
  • チームメンバーが複数パッケージを横断開発
  • TypeScript を定期的にバージョンアップ
  • CI/CD で厳格な型チェックを実施

逆に向いていないケース

一方で、以下のような状況では別のアプローチを検討すべきです。

  • パッケージ数が 2〜3 個の小規模プロジェクト(設定継承のオーバーヘッドが大きい)
  • 各パッケージが完全に独立している(統一のメリットが小さい)
  • フレームワーク固有の設定が多数必要(Next.js、Vite など)

導入を検討する際の推奨ステップ

もしこれから tsconfig.json の共有設計を導入するなら、以下のステップを推奨します。

  1. 現状の設定を棚卸し:各パッケージの tsconfig.json の差異を把握
  2. 小規模な試験導入:1〜2 個のパッケージで継承設計を試す
  3. strictモードの段階的有効化:新規パッケージから順次移行
  4. CI/CD での自動型チェック構築:Pull Request ごとに全パッケージを検証
  5. チーム全体での合意形成:設定変更の影響範囲を共有

最後に

TypeScript の設定管理は、プロジェクトの成長とともに複雑化します。しかし、適切な設計を早期に導入することで、長期的なメンテナンスコストを大幅に削減できます。

本記事で紹介した設計パターンは、筆者のプロジェクトでの試行錯誤の結果ですが、全てのプロジェクトに適用できるわけではありません。あなたのプロジェクトの規模、チーム構成、運用方針に応じて、最適な設計を選択してください。

@tsconfig/bases を基盤とした共有設計により、より生産的で安全な TypeScript 開発を実現できることを願っています。

関連リンク

著書

とあるクリエイター

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

;