1. ホーム
  2. TypeScript

【TypeScript】tsconfig.json の include・exclude・files の使い方|コンパイル対象のファイルを指定する

Share

npx tsc を実行したとき、TypeScript は「プロジェクトのどのファイルをコンパイル・型チェックするのか」をまず決めます。この対象を指定するのが tsconfig.jsonincludeexcludefiles という3つのオプションです。設定を書かないままだとテストファイルやビルド成果物まで巻き込んでコンパイルが遅くなり、逆に書き方を間違えると「No inputs were found」で何もコンパイルされません。この記事では、3つの既定値とワイルドカードの意味、そして「exclude に書いたのに型チェックされる」という定番のつまずきまでを整理します。

3つのオプションが決めているのは「出発点のファイル」

TypeScript のコンパイラは、まず対象となるファイルの一覧を作り、それを起点に import を辿って必要なファイルを集めます。この最初の一覧のことをルートファイル(root files)と呼びます。includeexcludefiles は、このルートファイルを決めるためのオプションです。

オプション役割ワイルドカード
include対象にするファイルをパターンで指定する使える
excludeinclude が拾ったものから除外するパターンを指定する使える
files対象にするファイルを1つずつ名指しで列挙する使えない

いちばんよく見るのは、src ディレクトリだけを対象にして、テストとビルド成果物を外す形です。パスはすべて、その tsconfig.json が置かれているディレクトリからの相対で解釈されます。

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "outDir": "dist",
    "strict": true
  },
  // src 配下のすべてを対象にする
  "include": ["src"],
  // ただしテストとビルド成果物は外す
  "exclude": ["node_modules", "dist", "src/**/*.test.ts"]
}

注意したいのは、これらが compilerOptions外側に書くオプションだという点です。stricttarget と同じ感覚で compilerOptions の中に書いてしまうと、「Object literal may only specify known properties」という警告が出るか、単に無視されます。

もうひとつ、npx tsc src/index.ts のようにコマンドラインでファイル名を直接渡すと、tsconfig.json そのものが読まれなくなります。設定を効かせたいときは、引数なしの npx tsc か、npx tsc -p tsconfig.build.json のように -p--project)で設定ファイルを指定してください。

include と exclude の既定値

3つとも省略できるオプションなので、書かなかったときにどう振る舞うかを知っておくと、既存プロジェクトの設定も読み解けるようになります。

オプション省略したときの既定値
files[](何も指定しない)
include["**/*"]。ただし files を書いている場合は []
exclude["node_modules", "bower_components", "jspm_packages"]、および outDir を指定していればその値

include を省略すると設定ファイルの隣が全部対象になる

includefiles も書かなかった場合、既定の ["**/*"] が効いて、tsconfig.json があるディレクトリとそのサブディレクトリの TypeScript ファイルがすべて対象になります。小さなプロジェクトならこれで困りませんが、リポジトリのルートに scripts/docs/ が同居していると、意図しないファイルまで型チェックされます。include を明示しておくほうが結果は安定します。

exclude を自分で書くと既定値は消える

見落としやすいのが、exclude を書いた時点で既定値が追加ではなく置き換えになることです。"exclude": ["dist"] とだけ書くと、node_modules の除外が外れてしまいます。

tsconfig.json(node_modules を拾ってしまう例)
{
  "compilerOptions": { "strict": true },
  // include を省略しているので "**/*" が効く
  // exclude を書いたため node_modules の除外が外れ、
  // node_modules 配下の .d.ts まで対象になってしまう
  "exclude": ["dist"]
}

この状態はコンパイルが極端に遅くなる原因になります。対処は単純で、exclude を書くときは node_modules も一緒に並べるか、そもそも "include": ["src"] のように対象側を絞ることです。includesrc しか見ていなければ、node_modules はパターンに引っかからないので問題は起きません。

outDir が既定の除外に入っているのも同じ理由です。dist に出力した .d.ts を次のコンパイルで入力として拾ってしまう、という自己参照を防いでくれています。

ワイルドカードの書き方

includeexclude で使えるワイルドカードは3種類だけです。シェルの glob とよく似ていますが、区別すべき点があるので表で整理します。

記号意味
*0個以上の文字にマッチする。ただしディレクトリ区切り(/)にはマッチしないsrc/*.tssrc/index.ts にマッチするが src/lib/a.ts にはマッチしない
?任意の1文字にマッチする。こちらもディレクトリ区切りは除くsrc/v?.tssrc/v1.ts にマッチする
**/任意の階層のディレクトリにマッチする(0階層も含む)src/**/*.tssrc/index.ts にも src/a/b/c.ts にもマッチする

* がディレクトリ区切りをまたがない、というのがいちばん重要な性質です。「src の中のファイルを全部」のつもりで "include": ["src/*.ts"] と書くと、サブディレクトリのファイルが丸ごと漏れます。階層を潜りたいときは必ず **/ を挟んでください。

ディレクトリ名だけを書いたとき

"include": ["src"] のようにワイルドカードを含まないディレクトリ名を書いた場合は、そのディレクトリ以下がすべて対象になります。つまり "src/**/*" と書いたのと同じ意味です。よく見かける短い書き方はこの省略記法です。

拡張子を書かないときに拾われるファイル

src/**/* のように拡張子を指定しないパターンでは、TypeScript が対応している拡張子のファイルだけが対象になります。具体的には .ts.tsx.d.ts で、allowJs を有効にしている場合はここに .js.jsx が加わります。src の中に画像や CSS が混ざっていても、それらが対象に入ることはありません。

逆に、拡張子を明示すると絞り込みになります。使い分けの目安を並べておきます。

include の書き方と対象範囲
{
  "include": [
    "src",              // src 以下すべて(src/**/* と同じ)
    "src/**/*",         // 同上。対応拡張子のファイルだけが入る
    "src/**/*.ts",      // .ts のみ。.tsx は入らない
    "src/**/*.{ts,tsx}" // ← これは動かない。ブレース展開は使えない
  ]
}

最後の行のようなブレース展開({ts,tsx})はサポートされていません。複数の拡張子を対象にしたいときは、["src/**/*.ts", "src/**/*.tsx"] と要素を分けて書くか、拡張子を書かずに ["src"] とします。

No inputs were found と言われたとき

パターンが1つもマッチしなかった場合、コンパイルは次のエラーで止まります。

ターミナル
error TS18003: No inputs were found in config file '/app/tsconfig.json'.
Specified 'include' paths were '["src"]' and 'exclude' paths were '["dist"]'.

エラーメッセージが includeexclude の中身をそのまま表示してくれるので、ここを読めば原因はだいたい絞れます。よくあるのは、ディレクトリ名の綴り違い、src/*.ts と書いてしまってサブディレクトリしか無かった、exclude のパターンが広すぎて include の結果を全部消していた、の3つです。

files はファイルを名指しで列挙する

files はワイルドカードを一切受け付けず、相対パスまたは絶対パスでファイルを1つずつ書き並べます。指定したファイルが存在しないとエラーになるので、「このプロジェクトはこの3ファイルから始まる」と宣言する用途に向いています。

tsconfig.json
{
  "compilerOptions": {
    "outDir": "dist",
    "strict": true
  },
  // ここから import を辿って必要なファイルが集められる
  "files": ["src/index.ts", "src/types/global.d.ts"]
}

ここに src/index.ts しか書いていなくても、そこから import されているファイルはすべてコンパイルされます。files が指定するのはあくまで出発点であって、プログラム全体のファイル一覧ではありません。ファイル数が少ないライブラリや、エントリポイントが明確なプロジェクトでは、これで十分に機能します。

一方で、どこからも import されないファイルは files に書かない限り対象外です。グローバルな型宣言を置いた global.d.ts のようなファイルを明示的に足しているのは、そのためです。

include と併用したときの挙動

filesinclude は同時に書けます。このとき対象になるのは、両方を合わせた和集合です。そして重要なのが、exclude が効くのは include が拾ったファイルに対してだけだという点です。files に書いたファイルは、exclude に該当していても除外されません。

tsconfig.json(files は exclude を無視する)
{
  // exclude で .test.ts を除いているが……
  "files": ["src/setup.test.ts"],
  "include": ["src"],
  "exclude": ["src/**/*.test.ts"]
}

// 結果:
//   src/setup.test.ts → 対象になる(files に書いたため)
//   src/foo.test.ts   → 対象にならない(include 経由なので exclude が効く)

矛盾しているように見えますが、「exclude はパターンで広く拾ったものを削るためのフィルタであり、名指しした files より優先度が低い」と考えると筋が通ります。全体からテストを外しつつ1ファイルだけ例外的に含めたい、という細かい調整に使えます。

exclude に書いたファイルが型チェックされる理由

exclude でいちばん多い誤解が、「ここに書けばそのファイルは絶対にコンパイルされない」というものです。実際には excludeinclude の結果を絞るだけで、そのファイルがプログラムに入ることを禁止する仕組みではありません

import されたファイルは取り込まれる

コンパイラはルートファイルから import を辿って依存を集めます。このとき exclude は参照されません。つまり、除外したはずのファイルでも、対象のファイルから import されていれば普通に読み込まれ、型チェックされ、エラーがあれば報告されます。

src/index.ts
// tsconfig.json で "exclude": ["src/legacy"] としていても、
// この import があるかぎり legacy/old.ts は型チェックされる
import { calc } from './legacy/old';

console.log(calc(1, 2));

同じことは types オプションによる型定義の読み込みや、/// <reference path="..." /> ディレクティブによる参照でも起こります。そして先ほど見たとおり、files に書いたファイルも exclude を素通りします。

本当に外したいなら参照を切る

あるファイルを確実にコンパイル対象から外す唯一の方法は、そこへの参照を無くすことです。import を削る、あるいは古いコードをディレクトリごと src の外へ移して include の範囲から出す、といった対応になります。設定だけで解決しようとすると堂々巡りになるので、依存関係のほうを見直してください。

どうしても参照を残したまま型エラーを止めたい場合は、該当行に // @ts-expect-error を付けるか、ファイル先頭に // @ts-nocheck を書いてそのファイルの型チェックだけを無効にする方法があります。いずれも一時的な回避策なので、移行期間中の手当てとして使うのが現実的です。

node_modules の型定義でエラーが出るとき

エラーの出どころが自分のコードではなく node_modules 配下の .d.ts だった場合、excludenode_modules を足しても解決しません。ライブラリの型定義は import から辿られて読み込まれているからです。

tsconfig.json
{
  "compilerOptions": {
    // .d.ts ファイル自体の型チェックを飛ばす
    "skipLibCheck": true
  },
  "include": ["src"]
}

ここで使うのが skipLibCheck です。これは .d.ts ファイルの中身の型チェックをスキップするオプションで、ライブラリ同士の型定義が衝突している場合の定番の対処です。自分のコードから型定義を使う際のチェックは通常どおり行われるため、副作用は限定的です。tsc --init が生成する設定でも既定で有効になっています。

用途ごとに tsconfig を分ける

「ビルドにはテストを含めたくないが、テストの型チェックはしたい」のように要求が二つある場合、1つの tsconfig.json で両立させようとすると無理が出ます。この場合は設定ファイル自体を分けるのが素直な解決策です。次の節で具体的な形を見ていきます。

エディタだけエラーが出るとき

npx tsc --noEmit は通るのに VS Code には赤線が出る、という場合は、そのファイルがどの tsconfig.json の対象にも入っていない可能性があります。エディタは設定に属さないファイルを開くと、既定値による推論プロジェクトとして扱って型チェックを行うためです。include の範囲を見直して、開いているファイルが対象に入っているかを確認してください。

extends したときの相対パスの解決

共通設定を親ディレクトリに置いて extends で読み込む構成では、パスの解決規則を知らないとハマります。ルールはひとつで、設定ファイル内の相対パスは、そのパスが書かれているファイルの位置を基準に解決されるというものです。

tsconfig.base.json(リポジトリのルート)
{
  "compilerOptions": {
    "target": "ES2022",
    "strict": true,
    "skipLibCheck": true
  },
  // この "src" はリポジトリのルート直下の src と解釈される
  "include": ["src"]
}
packages/api/tsconfig.json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "dist"
  },
  // 継承先で書き直さないと packages/api/src が対象にならない
  "include": ["src"]
}

継承元に書いた "include": ["src"] は、継承元のファイルがある場所、つまりリポジトリのルートの src を指したままです。packages/apisrc にはなりません。そのため、モノレポの共通設定に includeexclude を書くのは避け、compilerOptions だけを共有して、対象ファイルの指定は各パッケージ側に書くのが安全です。

もうひとつ覚えておきたいのは、filesincludeexclude は継承先で書くと継承元の値を上書きするということです。compilerOptions のようにプロパティ単位でマージされるのではなく、配列ごと丸ごと置き換わります。共通設定の exclude に足すつもりで書くと、元の除外が消えてしまうので注意してください。

実践的な設定例

src だけをコンパイルしてテストを外す

もっとも一般的な構成です。includesrc を丸ごと拾い、exclude でテストファイルだけを落とします。テストは Vitest や Jest が独自に変換するので、tsc の出力には含める必要がありません。

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["src"],
  "exclude": [
    "node_modules",
    "dist",
    "src/**/*.test.ts",
    "src/**/*.spec.ts",
    "src/**/__tests__"
  ]
}

rootDirsrc にしているのは、出力されるディレクトリ構造を安定させるためです。これを書かないと、コンパイル対象に src の外のファイルが混ざった瞬間に出力先が dist/src/... へずれます。rootDir を明示しておけば、範囲外のファイルが入ってきたときに TS6059 というエラーで気付けます。

ビルド用と型チェック用を分ける

テストのコードも型チェックはしたい、という要求には、設定ファイルを2つ用意して応えます。普段使う tsconfig.json はテストまで含めた広い範囲を見て型チェックだけを行い、ビルド専用の tsconfig.build.json がテストを除外して出力を担当する、という分担です。

tsconfig.json(エディタと型チェック用)
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "skipLibCheck": true,
    // 出力はしない。型チェック専用
    "noEmit": true
  },
  // テストや設定ファイルも型チェックの対象に含める
  "include": ["src", "tests", "vitest.config.ts"],
  "exclude": ["node_modules", "dist"]
}
tsconfig.build.json(出力用)
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src",
    "declaration": true,
    // 継承元の noEmit を打ち消して出力を有効にする
    "noEmit": false
  },
  // 継承元の include を上書きして src だけに絞る
  "include": ["src"],
  "exclude": ["node_modules", "dist", "src/**/*.test.ts"]
}

この2つのファイルは同じディレクトリにあるので、extends の相対パス問題は起きません。includeexclude はどちらも継承元の値を上書きするため、ビルド側では改めて書き直しています。

package.json
{
  "scripts": {
    "typecheck": "tsc --noEmit",
    "build": "tsc -p tsconfig.build.json"
  }
}

実際に対象になっているファイルを確認する

設定を書き換えたら、思ったとおりのファイルが対象になっているかを確かめておくと確実です。コンパイラ自身に一覧を出させるオプションが2つ用意されています。

ターミナル
# コンパイル対象のファイルを一覧表示する(出力はしない)
npx tsc --listFilesOnly

# node_modules を除いた自分のコードだけを見る
npx tsc --listFilesOnly | grep -v node_modules

# extends を解決したあとの最終的な設定を表示する
npx tsc --showConfig

--listFilesOnly は、import を辿って集められた分も含めた最終的なファイル一覧を出します。exclude したはずのファイルがここに現れたら、どこかから参照されているということです。--showConfig のほうは extends をすべて展開したあとの設定を JSON で表示するので、モノレポでどの値が最終的に効いているかを調べるのに向いています。

まとめ

include はパターンで対象ファイルを拾い、exclude はその結果から不要なものを落とし、files はワイルドカードを使わずにファイルを名指しします。include を省略すると **/*exclude を省略すると node_modulesbower_componentsjspm_packagesoutDir が既定値になります。exclude を自分で書くとこの既定値は置き換わるので、node_modules を書き忘れないようにしてください。

ワイルドカードは *(ディレクトリ区切りを除く0文字以上)、?(同じく1文字)、**/(任意の階層)の3つだけです。階層を潜るには **/ が必要で、src/*.ts ではサブディレクトリが漏れます。拡張子を書かないパターンでは、.ts.tsx.d.tsallowJs 有効時は .js.jsx)だけが対象になります。

そして最大のポイントは、exclude が効くのは include の結果に対してだけ、という点です。import されているファイルや files に書いたファイルは、exclude に該当していてもプログラムに取り込まれます。本当に外したいなら参照そのものを切るか、用途ごとに tsconfig.json を分けてください。迷ったら npx tsc --listFilesOnlynpx tsc --showConfig で現状を確かめるのが近道です。

参考ページ