monorepo で共通型定義パッケージを楽に利用する
概要
所属しているプロジェクトではフロントエンドもバックエンドも TypeScript で開発しているのですが、とある理由から最近 yarn workspaces を使った monorepo へ移行しました。フロントエンドとバックエンドに共通するロジックはなかったのですが、共通する型はあったので型定義場所を作りたくなり、その際に調べた知識と設定の仕方を共有したいと思います。
プロジェクトでは yarn workspaces を利用していますが、検証時は npm workspaces でも試したのでどちらの monorepo tool でも問題なく動作するはずです。また、サンプルコード等は説明に必要な最低限のものに省略したのですが、もし必要なコードが抜けている等あれば教えて頂けると助かります。
前提知識
npm パッケージの扱い
- パッケージ名は
package.jsonのnameに指定したものが使われる- name に
hogeと指定したらnode_modules/hogeにインストールされる - name に
hoge/fugaと指定したらnode_modules/hoge/fugaにインストールされる
- name に
- 公開ライブラリの @types 系のパッケージは
DefinitelyTypedというリポジトリで行われていることが多い- ここ: https://github.com/DefinitelyTyped/DefinitelyTyped
- このリポジトリ自体が特別扱いされているわけではない
-
@types/xxxxという名前でパッケージを公開する仕組みが整っている感じ
つまり DefinitelyTyped を使わなくても package.json の name に @types/hoge という名前をつけると node_modules/@types/hoge にインストールされる。
npm/yarn workspaces の仕組み
- ルートとなるディレクトリにある
package.jsonの workspaces に指定したものがサブプロジェクトとして認識される - ルートで
npm installかyarn installを実行すると workspaces に指定されたものがnode_modulesにインストールされる- 実態はただのシンボリックリンク
- この時のインストール先は前述の npm パッケージのインストール方法に従って行われる
つまり root/types/package.json の name に @types/hoge と指定されていたら root/node_modules/@types/hoge に root/types へのシンボリックリンクが作られる。
@types の認識範囲
TypeScript: TSConfig Reference - #typeRoots
TypeScript: Documentation - Module Resolution
上記 2 つのドキュメントに書かれた具体例だけ見ると node_modules/@types や ../node_modules/@types などのようにディレクトリを遡りながら node_modules の中にある @types だけを認識するっぽく解釈してしまいそうになるけど実際は違う。
By default all visible ”@types” packages are included in your compilation.
tsconfig.json の typeRoots の説明にあるこの最初の一文が正しくて結構広い範囲にある @types ディレクトリは勝手に認識してくれる。
api-server
├── @types
│ └── index.d.ts
├── src
│ ├── @types
│ │ └── index.d.ts
│ ├── models
│ │ └── index.ts
│ └── index.ts
├── package.json
└── tsconfig.json
試した範囲だと上記のような構成で tsconfig.json の設定を "include": "**" にすると api-server/@types も api-server/src/@types も認識くれました。
設定方法
ここまでの前提知識を踏まえ、以下のようなよくありそうな client/server の monorepo 構成で types に共通の型定義を行いたい場合の例です。
project-root
├── api-server
│ ├── src
│ │ ├── @types
│ │ │ └── index.d.ts
│ │ └── index.ts
│ ├── package.json
│ └── tsconfig.json
│
├── web-client
│ ├── src
│ │ ├── @types
│ │ │ └── index.d.ts
│ │ └── index.ts
│ ├── package.json
│ └── tsconfig.json
│
├── types
│ ├── src
│ │ └── index.d.ts
│ ├── package.json
│ └── tsconfig.json
│
└── package.json
リポジトリルートの package.json に workspaces の設定を行う
// repository-root/package.json
{
"name": "hoge",
"private": true,
"workspaces": [
"types",
"api-server",
"web-client"
]
}
types/package.json のパッケージ名を @types/xxxxx にする
// repository-root/types/package.json
{
"name": "@types/hoge",
"types": "src/index.d.ts",
"private": true
}
リポジトリルートで npm install または yarn install すれば、リポジトリルートの node_modules/@types/hoge にシンボリックリンクが作られて認識してくれるようになります。
これだけで types に共通の型定義、api-server と web-client の src/@types にそれぞれ必要な型定義をすれば import なしで認識してくれます。
おまけ
// repository-root/types/tsconfig.json
{
"compilerOptions": {
"target": "es2021",
"module": "commonjs",
"rootDir": "./src",
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noUnusedLocals": false, //未使用チェックをしない
"noFallthroughCasesInSwitch": true,
"skipLibCheck": false //.d.tsの型チェックを有効化
},
"include": [ "src" ]
}
最後におまけ程度ですが types の tsconfig.json です。
プロダクトコード側だと tsc --init で生成されるデフォルトの skipLibCheck: true にする場合が多いと思いますが、ここでは型定義しかしないので .d.ts ファイルでも型チェックが行われるようにしています。
Discussion