🎉

monorepo で共通型定義パッケージを楽に利用する

2022/07/06に公開

概要

所属しているプロジェクトではフロントエンドもバックエンドも TypeScript で開発しているのですが、とある理由から最近 yarn workspaces を使った monorepo へ移行しました。フロントエンドとバックエンドに共通するロジックはなかったのですが、共通する型はあったので型定義場所を作りたくなり、その際に調べた知識と設定の仕方を共有したいと思います。

プロジェクトでは yarn workspaces を利用していますが、検証時は npm workspaces でも試したのでどちらの monorepo tool でも問題なく動作するはずです。また、サンプルコード等は説明に必要な最低限のものに省略したのですが、もし必要なコードが抜けている等あれば教えて頂けると助かります。

前提知識

npm パッケージの扱い

  • パッケージ名は package.jsonname に指定したものが使われる
    • name に hoge と指定したら node_modules/hoge にインストールされる
    • name に hoge/fuga と指定したら node_modules/hoge/fuga にインストールされる
  • 公開ライブラリの @types 系のパッケージは DefinitelyTyped というリポジトリで行われていることが多い

つまり DefinitelyTyped を使わなくても package.jsonname@types/hoge という名前をつけると node_modules/@types/hoge にインストールされる。

npm/yarn workspaces の仕組み

  • ルートとなるディレクトリにある package.json の workspaces に指定したものがサブプロジェクトとして認識される
  • ルートで npm installyarn install を実行すると workspaces に指定されたものが node_modules にインストールされる
    • 実態はただのシンボリックリンク
    • この時のインストール先は前述の npm パッケージのインストール方法に従って行われる

つまり root/types/package.jsonname@types/hoge と指定されていたら root/node_modules/@types/hogeroot/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/@typesapi-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