TypeScriptを使用した開発において、ソースコードの管理とビルド成果物の整理は、プロジェクトの保守性を高めるための非常に重要な要素です。

デフォルトの設定では、TypeScriptコンパイラ (tsc) はコンパイルされたJavaScriptファイルを元のTypeScriptファイルと同じディレクトリに出力します。

しかし、大規模なプロジェクトやモダンなフロントエンド開発において、ソースコードと実行コードが混在することは推奨されません。

そこで活用されるのが、ビルド成果物の出力先を制御する outDir オプションです。

この記事では、tsconfig.json における outDir の正しい設定方法から、関連する設定項目、そして運用上の注意点までを詳しく解説します。

TypeScriptにおけるoutDirの役割

TypeScriptのコンパイルプロセスにおいて、outDir は「生成されたすべてのJavaScriptファイル、ソースマップ、および型定義ファイルをどのディレクトリに配置するか」を決定する司令塔の役割を果たします。

通常、開発者は src ディレクトリ内にTypeScriptファイルを記述します。

もし outDir を設定せずにコンパイルを実行すると、src/index.ts から生成された src/index.js が同じ場所に作成されてしまいます。

これにより、ソースコードの管理が煩雑になり、誤ってコンパイル後のファイルを編集してしまったり、Git管理下での差分確認が困難になったりするリスクが生じます。

outDir を適切に設定することで、ソースコード(Source)とビルド成果物(Artifacts)を完全に分離し、クリーンなプロジェクト構造を維持することが可能になります。

これは、CI/CDパイプラインの構築や、成果物のみをサーバーにデプロイする際にも不可欠なプロセスです。

tsconfig.jsonでの基本的な設定方法

outDir を設定するには、プロジェクトのルートディレクトリにある tsconfig.json ファイルを編集します。

設定の記述例

以下は、一般的なプロジェクト構成でよく用いられる設定例です。

JSON
{
  "compilerOptions": {
    /* 出力先ディレクトリを "dist" に指定 */
    "outDir": "./dist",
    
    /* モジュール形式やターゲットの設定 */
    "target": "ESNext",
    "module": "NodeNext",
    
    /* ソースコードのルートを明示的に指定 */
    "rootDir": "./src",
    
    /* 厳格な型チェックを有効化 */
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.test.ts"]
}

この設定により、src ディレクトリ内のTypeScriptファイルがコンパイルされ、その結果がすべて dist ディレクトリに出力されます。

ディレクトリ構造の変化

具体的にどのようなファイル構成になるかを確認しましょう。

例えば、以下のようなソース構成の場合を想定します。

ファイルパス説明
src/index.tsメインエントリポイント
src/utils/math.tsユーティリティ関数
src/components/App.tsxUIコンポーネント

tsc コマンドを実行した後の出力結果は、以下のようになります。

text
project-root/
├── dist/
│   ├── index.js
│   ├── utils/
│   │   └── math.js
│   └── components/
│       └── App.js
├── src/
│   ├── index.ts
│   ├── utils/
│   │   └── math.ts
│   └── components/
│       └── App.tsx
├── tsconfig.json
└── package.json

src 内のディレクトリ構造がそのまま dist 内に再現されていることがわかります。

これにより、プログラム内部での相対パスによるモジュール参照が壊れることなく、正常に動作するJavaScriptが生成されます。

rootDirとの密接な関係

outDir を理解する上で欠かせないのが rootDir オプションです。

この2つはセットで考える必要があります。

rootDirを指定しない場合の挙動

rootDir を明示的に指定しない場合、TypeScriptコンパイラは コンパイル対象となる全ファイルの共通のベースディレクトリ を自動的に計算し、それをルートとみなします。

しかし、この挙動には注意が必要です。

例えば、プロジェクトのルートにテスト用のダミーデータファイル test-data.ts を誤って作成し、それがコンパイル対象に含まれてしまった場合、ベースディレクトリが意図せず上にずれてしまい、dist の中に src/ という名前のフォルダが作られてしまうことがあります。

rootDirを指定するメリット

rootDir を指定しておくことで、コンパイラに対して「ここがソースコードの起点である」と明確に伝えることができます。

もし rootDir の範囲外にあるファイルがコンパイル対象に含まれると、TypeScriptはコンパイルエラーを出力します。

「意図しないディレクトリ構造が dist 内に作成される」というトラブルを防ぐためにも、rootDir と outDir は常にセットで定義することを推奨します。

JSON
{
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

この設定により、src フォルダの内容のみが dist の直下に配置されることが保証されます。

複数の出力先が必要なケース(ライブラリ開発など)

Webアプリケーションの開発ではなく、ライブラリの開発を行っている場合、複数のモジュール形式(CommonJSとES Modulesの両方など)を提供したいケースがあります。

この場合、1つの tsconfig.json では対応しきれないため、複数の設定ファイルを使い分けます。

CommonJS用の設定 (tsconfig.cjs.json)

JSON
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "CommonJS",
    "outDir": "./dist/cjs"
  }
}

ES Modules用の設定 (tsconfig.esm.json)

JSON
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "ESNext",
    "outDir": "./dist/esm"
  }
}

このように extends を利用して基本設定を継承しつつ、outDir をそれぞれ dist/cjsdist/esm に分けることで、配布用のパッケージ構成をきれいに整えることができます。

実践的な運用における注意点

outDir を活用する際には、いくつかの運用上の落とし穴があります。

これらを事前に把握しておくことで、ビルドエラーや実行時エラーを回避できます。

1. ビルド前のディレクトリクリーニング

TypeScriptコンパイラは、コンパイルのたびに outDir 内の古いファイルを自動で削除してくれるわけではありません。

古いJavaScriptファイルが残っていると、TypeScript側でソースを削除したにもかかわらず、コンパイル後の古いファイルが残っていて実行できてしまうという「ゴーストファイル」の問題が発生します。

これを解決するために、ビルドコマンドを実行する前にディレクトリをクリアする習慣をつけましょう。

一般的には rimraf などのパッケージを利用します。

Shell
# package.json の scripts 例
{
  "scripts": {
    "prebuild": "rimraf dist",
    "build": "tsc"
  }
}

ビルド前に dist を削除することで、常に最新のソースコードに基づいた正確な成果物だけを維持できます。

2. TypeScript以外のファイルの扱い

outDir はあくまで tsc が生成するファイル(.js, .d.ts, .js.map)の出力先を指定するものです。

プロジェクトに含まれる画像ファイル、CSS、JSONファイルなどの非TypeScript資産は、outDir に自動でコピーされません。

これらのファイルを扱うには、以下のいずれかの方法を検討する必要があります。

  • ビルドスクリプト(cpコマンドや静的ファイルのコピーライブラリ)を使用して手動でコピーする。
  • WebpackやVite、esbuildなどのビルドツール(バンドラー)を併用し、それらのアセット管理機能を利用する。

モダンな開発環境では、tsc は型チェックと型定義ファイルの生成に専念させ、コードのトランスパイルとアセットの集約は Vite 等の高速なバンドラーに任せる構成が一般的です。

その場合でも、型定義ファイル(.d.ts)の出力先として outDir は重要な役割を果たし続けます。

3. path alias(パスエイリアス)との競合

tsconfig.jsonpaths オプションを使用してパスのエイリアス(例: @/components/...)を設定している場合、outDir に出力されたJavaScriptファイル内のインポートパスが解決されないという問題が頻発します。

TypeScriptコンパイラ自体はパスエイリアスを解決して書き換える機能を持ちません。

実行時に解決するためには、tsconfig-paths を使用するか、ビルドプロセスでパスを置換するツールを組み込む必要があります。

この点は outDir を導入する際に必ず突き当たる壁であるため、事前に方針を決めておくことが重要です。

型定義ファイル (.d.ts) の出力制御

ライブラリ開発者にとって、実行コードと同じくらい重要なのが型定義ファイルです。

declaration: true を設定している場合、型定義ファイルも outDir に出力されます。

もし、型定義ファイルだけを別のディレクトリに出力したい場合は、declarationDir オプションを併用します。

JSON
{
  "compilerOptions": {
    "outDir": "./dist/js",
    "declaration": true,
    "declarationDir": "./dist/types"
  }
}

このように設定すると、JavaScriptファイルは dist/js に、型定義ファイルは dist/types に整理して出力されます。

プロジェクトの公開形式に合わせて最適な構造を選択しましょう。

よくあるエラーと解決策

outDir 設定時によく遭遇するエラーの代表例が 「Cannot write file ‘…’ because it would overwrite input file」 です。

このエラーは、入力ファイル(.ts)の出力先が、そのファイル自身と同じ場所になってしまう場合に発生します。

主な原因は以下の通りです。

  • outDir が設定されていない、またはプロジェクトルートに設定されている。
  • rootDiroutDir が同じパスに設定されている。
  • 誤って include パターンの中に outDir で指定したディレクトリが含まれてしまっている。

解決するには、tsconfig.json を見直し、ソースディレクトリ(src)と出力ディレクトリ(dist)が完全に分離されているかを確認してください。

また、exclude 設定に outDir のパスを追加することも有効な対策です。

まとめ

TypeScriptの outDir オプションは、単に出力先を変えるだけの機能ではなく、プロジェクトの構造を定義し、ビルドパイプラインを円滑にするための基盤となる設定です。

この記事で解説したポイントを振り返ります。

  • outDir を設定することで、ソースコードと成果物を明確に分離できる
  • rootDir と併用することで、意図しないディレクトリ構造の崩れを防ぐことができる。
  • ビルド前には dist ディレクトリをクリーニングする運用を推奨。
  • 複数のモジュール形式をサポートする場合は、設定ファイルの継承機能を活用する。
  • 静的アセットやパスエイリアスの扱いに注意が必要。

適切なディレクトリ設計は、プロジェクトの規模が大きくなればなるほどその価値を発揮します。

まずは標準的な srcdist の構成から始め、必要に応じて declarationDir や複数の設定ファイルを組み合わせていくのがベストプラクティスです。

設定一つで開発体験は大きく変わります。

ぜひ、ご自身のプロジェクトに最適な outDir 設定を見つけてください。