k-tokitoh

2025-05-10

typescriptのプロジェクト参照がわからない

怪しい理解を雑多に書きつける。

前段

tsconfig と tsc

tsc によるビルドの対象

% npx tsc --help
  tsc
  Compiles the current project (tsconfig.json in the working directory.)

複数のプロジェクト

(プロジェクト参照を利用しない場合)

- foo/
  - tsconfig.json
  - index.ts  // common/index.tsをimportする
- bar/
  - tsconfig.json
  - index.ts  // common/index.tsをimportする
- common/
  - index.ts
% npx tsc -p foo/tsconfig.json  // このときcommon/index.tsはfooというプロジェクトの一部としてビルドされる
% npx tsc -p bar/tsconfig.json  // このときcommon/index.tsはbarというプロジェクトの一部としてビルドされる

vscode と tsc

あるファイルに対する tsconfig が一意に定まる必要はない

インクリメンタルビルド

本題 - プロジェクト参照

メリット

1

- foo/
  - tsconfig.json
  - index.ts
- bar/
  - tsconfig.json
  - index.ts

プロジェクト参照を利用しない場合、以下の課題がある。

これをプロジェクト参照で解決できる。

ルートに(コンパイル対象をもたない空の)プロジェクトを作成し、そのプロジェクトから foo, bar を参照する。

- foo/
  - tsconfig.json
  - index.ts
- bar/
  - tsconfig.json
  - index.ts
- tsconfig.json <- NEW!

tsconfig.json

{
	"files": [], // このプロジェクト自体はコンパイル対象をもたない
	"references": [{ "path": "foo/tsconfig.json" }, { "path": "bar/tsconfig.json" }]
}

こうすればtsc --buildという 1 回のコマンドで、複数のプロジェクトをまとめてビルドできる。

2

続きで、foo, bar 両方 から共通のコードを参照する場合を考える。

- foo/
  - tsconfig.json
  - index.ts  // common/index.tsをimportする
- bar/
  - tsconfig.json
  - index.ts  // common/index.tsをimportする
- common/
  - index.ts
- tsconfig.json

以下の課題がある。

プロジェクト参照によってこれを解決できる。

- foo/
  - tsconfig.json
  - index.ts
- bar/
  - tsconfig.json
  - index.ts
- common/
  - tsconfig.json <- NEW!
  - index.ts
- tsconfig.json

foo/tsconfig.json 及び bar/tsconfig.json

{
	"references": [{ "path": "../common/tsconfig.json" }]
}

common/tsconfig.json

{
	"compilerOptions": {
		"composite": true,
		"emitDeclarationOnly": true
	}
}

最後のコードブロックでの設定について、引き続き説明する。

(コンパイル対象をもつプロジェクトから)参照されるプロジェクトに課される条件

プロジェクト参照では、コンパイル対象をもつプロジェクトから参照されるプロジェクトには一定の条件が課される。

上記のメリット 2 の例では、foo はfoo/**/*というコンパイル対象をもつため(bar も同様)、それらから参照される common にはこの条件が課されることになる。

他方で、上記のメリット 1 の例では、ルートのプロジェクトが"files": []という設定によりコンパイル対象をもたないため、そこから参照される foo, bar にはこの条件が課されない。

条件 1. composite

該当するプロジェクトは、"composite": trueと設定される必要がある。また、この設定によっていくつかの制約が発生する。

制約の詳細はdocsに詳しいが、代表的なのは「そのプロジェクトでのコンパイル対象全てが、filesまたはincludeでの指定範囲に含まれる必要がある」ことだ。

メリット 2 の例において、common プロジェクトでは include がデフォルトの**/*(common からの相対パス)になっている。 ここで common/index.tsから common 配下にないファイルを参照したとしよう。

(一部を抜粋)

- common/
  - tsconfig.json
  - index.ts  // another/index.tsをimportする
- another/
  - index.ts
common/index.ts:1:25 - error TS6307: File '/Users/USERNAME/ghq/github.com/k-tokitoh/test/another/index.ts' is not listed within the file list of project '/Users/USERNAME/ghq/github.com/k-tokitoh/test/foo/tsconfig.json'. Projects must list all files or use an 'include' pattern.

1 import { another } from "../another";
                          ~~~~~~~~~~~~

このエラーは以下いずれかの方法で解決できる。

ところで、files/include に関するこの制約はなぜ存在するのだろうか。適当な資料が見当たらなかったが、差し当たって以下のように考えている。

条件 2. d.ts の emit

コンパイル対象をもつプロジェクトから参照されるプロジェクトに課されるもうひとつの条件は「少なくとも d.ts を emit すること」である。

これは"noEmit": falseまたは"emitDeclarationOnly": trueなどの設定により満たすことができる。

ビルド全体がtsc --buildという単一のプロセスで実行されるのであれば、各プロジェクトの build 結果はディスクに書き出さずメモリでの保持で十分な気もするが、それではメモリ消費が肥大化してしまい、プロジェクト参照という企てが狙いとするビルドの効率化ができない、という判断なのだと思う。

例: npm create vite

今回の件を調べる発端は、npm create viteがプロジェクト参照する tsconfig を生成したことだった。

% npm create vite@6.5.0 test -- --template react-swc-ts
- tsconfig.json
- tsconfig.app.json
- tsconfig.node.json
- src/
  - ...
- vite.config.ts

tsconfig.json

{
	"files": [],
	"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
}

tsconfig.app.json(一部を抜粋)

{
	"compilerOptions": {
		"noEmit": true
	},
	"include": ["src"]
}

tsconfig.node.json(一部を抜粋)

{
	"compilerOptions": {
		"noEmit": true
	},
	"include": ["vite.config.ts"]
}

ここでの最大の関心は、app と node を異なる config でビルドすることだろう。

しかしそれだけであれば、プロジェクト参照を必ずしも利用しなくてよい。tsconfig.app.jsontsconfig.node.jsonの 2 つを配置して、それぞれに関してtscすれば実現できる。

しかし上記の構成では 2 つの config に加えて、それらを参照するtsconfig.jsonが配置されている。

これにより、まさに上述したメリット 1, すなわち「1 つのtsc --buildコマンドで複数のプロジェクトをビルドできる」が実現している。

他方で、上述のメリット 2, すなわち「共通するコードに対するビルド結果の再利用」は見出されない。