mizdra's blog

ぽよぐらみんぐ

tsconfig.json の include オプションには何を指定すべきか

TL;DR

  • "include": ["src/index.ts"] はやめよう
    • src 配下にあるのに型チェックされない & auto-import できないファイルが生まれてしまう
  • "include": ["src/**/*"]"include": ["**/*"] がオススメ
    • どっちが良いかはプロジェクトによる
    • "include": ["src/**/*"]"include": ["src"] と、"include": ["**/*"] は include 指定無しと同じなので、それでも OK
  • すっごい凝りたいなら Solution Style tsconfig.json を使おう

はじめに

tsconfig.jsoninclude オプションは、プロジェクトを構成するファイルを指定するオプションです。

例えば src/**/* を指定すると、src ディレクトリ以下の全てのファイル *1 がプロジェクトに所属するようになります。プロジェクトに所属するファイルは、型チェックやコンパイルの対象になります。

{
  "include": ["src/**/*"]
}
├── scripts
│   └── lint.ts         ⨯
├── src
│   ├── util
│   │    └── log.ts     ✓
│   ├── index.ts        ✓
│   └── math.ts         ✓
├── package.json
├── tsconfig.json
└── vitest.config.ts    ⨯

人によっては "include": ["src/**/*"] 以外にも、"include": ["src"], "include": ["src/index.ts"], "include": ["**/*"] などと指定することもあります。一見するとどれも同じように見えますが、実は挙動が異なります。この記事では、include オプションの指定方法によってどのような挙動になるのか、どう指定するのが良いのかについて考えてみます。

include オプションとそれに関連する仕様について

本題に入る前に、まずは include オプションやそれに関連する仕様について説明します。

プロジェクトに所属しないファイルは tsc -p tsconfig.json による型チェックの対象にならない

プロジェクトに所属しないファイルは tsc -p tsconfig.json による型チェックの対象になりません。先ほどの例で言うと、scripts/lint.tsvitest.config.tstsc コマンドによる型チェックの対象にならず、tsc -p tsconfig.json で型エラーが報告されません。

プロジェクトに所属しないファイルでも、エディタ上で型チェックされる

ややこしいことにプロジェクトに所属しないファイルも、エディタ上では型チェックの対象になります (厳密にはエディタではなく、「tsserver」と呼ばれる TypeScript の Language Server 実装に由来する振る舞いです。)。そのため、エディタ上では型エラーが報告されるものの、tsc -p tsconfig.json では報告されません。

また、型チェックに使われる設定は tsconfig.json で定義されているものではなく、tsserver 組み込みのデフォルトの設定が使われます *2。そのため、tsconfig.json"noImplicitAny": true を設定していても、デフォルトの設定である "noImplicitAny": false 相当で型チェックが行われてしまいます。

その結果、「エディタでは型エラーが出ているけど、CI を pass してしまう」「普段と異なる設定で型チェックが行われてしまう」といった問題に繋がりがちです。

プロジェクトに所属しないファイルは auto-imports の対象にならない

TypeScript には auto-imports という機能があります。auto-imports は、他のファイルに定義されている変数や関数を使おうとしたときに、補完の候補に出して、自動的に import 文を挿入してくれる機能です。

add 関数が補完の候補に出てきて、選択すると src/math.ts の import 文が挿入される様子

しかし、プロジェクトに所属しないファイルは auto-imports の対象になりません。もし仮に src/math.ts がプロジェクトに所属していない場合、src/math.ts に定義されている関数を使おうとしても、補完候補に出てきません。

add 関数が補完候補に出てこない様子

include にマッチしなくても、include にマッチするファイルから import されていればプロジェクトに所属する

ややこしいことに、include にマッチしないファイルでも、include にマッチするファイルから import されていればプロジェクトに所属すると見なされます。

例えば、以下のようなリポジトリがあるとします。

{
  "include": ["src/index.ts"]
}
├── src
│   ├── util
│   │    └── log.ts
│   ├── index.ts
│   └── math.ts
└── tsconfig.json
// src/index.ts
import { add } from "./math.js";

src/index.ts から math.ts が import されていますから、math.ts はプロジェクトに所属すると見なされます。そのため、math.tstsc -p tsconfig.json による型チェックの対象になりますし、auto-imports の対象にもなります。一方、util/log.ts はプロジェクトに所属しないと見なされます。

つまり include オプションにマッチしないからといって、プロジェクトに所属しないファイルとは限らないということです。

"include": ["src/**/*"]"include": ["src"] は同じ

TypeScript のソースコードを読んでみると、"include": ["src"]"include": ["src/**/*"] と同義のようでした。

"include" が指定されていない場合は "include": ["**/*"] と同じ

TypeScript のソースコードを読んでみると、"include" が指定されていない場合は "include": ["**/*"] と同じ挙動になるようでした。

結局 include オプションには何を指定したら良いの?

以下のようなディレクトリ構成をもとに話を進めます。

├── scripts
│   └── lint.ts
├── src
│   ├── util
│   │    └── log.ts
│   ├── index.ts
│   └── math.ts
├── package.json
├── tsconfig.json
└── vitest.config.ts
// src/index.ts
console.log('Hello, world!');

悪い例: "include": ["src/index.ts"]

src/math.tssrc/util/log.ts がプロジェクトに所属しないとファイルと見なされてしまい、tsc -p tsconfig.json で型チェックされなかったり、auto-imports の対象にならなかったりといった問題が発生します。この設定は絶対に避けるべきです。

プロジェクトに含まれるファイル:

├── scripts
│   └── lint.ts         ⨯
├── src
│   ├── util
│   │    └── log.ts     ⨯
│   ├── index.ts        ✓
│   └── math.ts         ⨯
├── package.json
├── tsconfig.json
└── vitest.config.ts    ⨯

悪い例: "include": ["src/*"]

先ほどと違い、src/math.ts はプロジェクトに所属すると見なされますが、src/util/log.ts は所属しないと見なされます。これも避けるべきです。

プロジェクトに含まれるファイル:

├── scripts
│   └── lint.ts         ⨯
├── src
│   ├── util
│   │    └── log.ts     ⨯
│   ├── index.ts        ✓
│   └── math.ts         ✓
├── package.json
├── tsconfig.json
└── vitest.config.ts    ⨯

良い例: "include": ["src/**/*"] or "include": ["src"]

src 配下の全てのファイルがプロジェクトに所属すると見なされ、tsc による型チェックや auto-imports の対象になります。

プロジェクトに含まれるファイル:

├── scripts
│   └── lint.ts         ⨯
├── src
│   ├── util
│   │    └── log.ts     ✓
│   ├── index.ts        ✓
│   └── math.ts         ✓
├── package.json
├── tsconfig.json
└── vitest.config.ts    ⨯

scripts/lint.tsvitest.config.ts は型チェックや auto-imports の対象になりませんが...そもそもこうしたファイルを src と同じ設定で型チェックしたいかというと、場合によると思います。例えば、src 配下がブラウザで実行されるファイルが置かれていたら、src 配下は "lib": ["ESNext", "DOM"] で、それ以外の Node.js で実行されるファイルは "lib": ["ESNext"] で型チェックしたいかもしれません。そうしたケースでは、むしろ scripts 配下や vitest.config.ts が含まれないこのパターンが適しているでしょう。

余談: scripts/lint.tsvitest.config.ts を別の設定で型チェックしたい場合

tsconfig.json を Node.js 向けの設定ファイルとして使い、src 配下のファイルだけを型チェックする設定ファイル (src/tsconfig.json) を別途用意すると良いです。

├── scripts
│   └── lint.ts         ✓ included by tsconfig.json
├── src
│   ├── util
│   │    └── log.ts     ✓ included by src/tsconfig.json
│   ├── index.ts        ✓ included by src/tsconfig.json
│   ├── math.ts         ✓ included by src/tsconfig.json
│   └── tsconfig.json   ✓ included by src/tsconfig.json
├── package.json
├── tsconfig.json
└── vitest.config.ts    ✓ included by tsconfig.json
// tsconfig.json
{
  "exclude": ["src"],
  "compilerOptions": {
    "lib": ["ESNext"]
  }
}
// src/tsconfig.json
{
  "compilerOptions": {
    "lib": ["ESNext", "DOM"]
  }
}

src/tsconfig.json のように src ディレクトリの中にあると取り回しづらいから、tsconfig.browser.json に rename してプロジェクトルートに置きたい...と思うかもしれませんが、素朴にやってしまうと tsserver がから検出できなくなってしまいます (詳しくは以下の記事を参照)。

もしプロジェクトルートに設定ファイルを集約したければ、Solution Style tsconfig.json というテクニックを使うと良いです。

├── scripts
│   └── lint.ts         ✓ included by tsconfig.node.json
├── src
│   ├── util
│   │    └── log.ts     ✓ included by tsconfig.browser.json
│   ├── index.ts        ✓ included by tsconfig.browser.json
│   └── math.ts         ✓ included by tsconfig.browser.json
├── package.json
├── tsconfig.json
├── tsconfig.browser.json
├── tsconfig.node.json
└── vitest.config.ts    ✓ included by tsconfig.node.json
// tsconfig.json
{
  "files": [],
  "references": [
    { "path": "./tsconfig.browser.json" },
    { "path": "./tsconfig.node.json" }
  ]
}
// tsconfig.browser.json
{
  "include": ["src"],
  "compilerOptions": {
    "lib": ["ESNext", "DOM"]
  }
}
// tsconfig.node.json
{
  "exclude": ["src"],
  "compilerOptions": {
    "lib": ["ESNext"]
  }
}

良い例: "include": ["**/*"] or include 指定なし

全てのファイルがプロジェクトに所属すると見なされ、tsc による型チェックや auto-imports の対象になります。src 配下が Node.js で実行するファイルであり、scripts 配下や vitest.config.ts とまとめて型チェックしてしまって問題ない場合は、これで良いでしょう。

プロジェクトに含まれるファイル:

├── scripts
│   └── lint.ts         ✓
├── src
│   ├── util
│   │    └── log.ts     ✓
│   ├── index.ts        ✓
│   └── math.ts         ✓
├── package.json
├── tsconfig.json
└── vitest.config.ts    ✓

参考

検証に使ったリポジトリを置いておきます。

github.com

*1:厳密には、src ディレクトリ以下の全てのファイルのうち、.ts/.mts/.cts で終わるファイルのマッチします。allowJS や checkJS オプションが有効な場合は、.js/.mjs/.cjs にもマッチします。

*2:プロジェクトに所属しないファイルは、tsserver の内部で「inferred project」という特殊なプロジェクトに属すると見なして型チェックが行われています。

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.