内部モジュールでは TypeScript をコンパイルせずに配る
主張
現代では、 npm に publish するものを除けば TypeScript をコンパイルして配布する必要はほぼないと考えています。
TypeScript のコンパイルをしない環境、例えば deno や rome を使うと、コンパイルをするというステップが省くことで開発体験が大きく向上する、という肌感があり、ローカル環境ならこのメリットを優先するべきと考えています。
基本的に、ビルドはフロントエンド/サーバーともに配布前の最後の一回だけでいいはず、というのが自分の主張です。
理由
現代の npm のお作法に従うと TypeScript に依存したものを npm の registry に置くことは、あまりよくないこととされていますが、モノレポ内や特定社内で使う github package registry 内部モジュールでは、その利用者の中でコンセンサスが得られるならその縛りはないはずです。
packages/
foo/
index.ts
package.json
bar/
index.ts
package.json
tsconfig.json
package.json
(このとき、 foo の package.json の main では "main": "index.ts"
のように指定することも可能です)
TypeScript はだいぶ仕様が安定してきています。babel の preset-typescript, vite で使われる esbuild, deno で使われる swc, マイナーどころでは sucrase 等で様々な TypeScript コンパイラが出現し、これらが実運用されるようになったことで、 「TypeScript のベースラインと言えばこの仕様を満たすもの」というコンセンサスが、ある程度決まってきています。
- evanw/esbuild: An extremely fast JavaScript bundler and minifier
- swc-project/swc: swc is a super-fast compiler written in rust; producing widely-supported javascript from modern standards and typescript.
- Sucrase
MS の TypeScript 自体も後方互換はほぼ崩れませんし、そもそも現代の node/モダンブラウザ環境で es2021 水準では typescript の型を除去する部分しか使わず、トランスパイルによる方言が表出することも減っています。
やむにやまれぬ IE 対応の際も、 ts-loader とは別に多段で設定された babel-loader で行われることも多いので、TypeScript の責務としてes5トランスパイルは強く要求されていません。
tsconfig の厳密さが異なる場合に モジュール利用側で型が壊れていても、 最悪 "skipLibCheck": true
で無視することができます。
TypeScript をそのまま配るメリット
- モジュール側の変更の反映のために再ビルドしないでいい
- ちょっと面倒な
.d.ts
のビルド設定の必要がない - 厄介な sourcemap 対応が必要ない
- vscode intellisense で
.d.ts
の型定義ではなく実装にジャンプできる - commonjs | esm どちらでビルドするかは利用側の tsconfig で決めることができる
- 利用側で好きな TypeScript コンパイラを選ぶことができる
TypeScript をそのまま配るデメリット
- モジュール側で typescript の最新機能を使う場合は、利用側のバージョンも上げる必要がある
- tsconfig の設定が噛み合わないとコンパイルエラーになることがある
- node.js server の場合、 runtime に esbuild 等が必要になる
- 超巨大プロジェクトなら無視できないオーバーヘッドになるかも
- React 以外の jsx は全てのファイルで jsx pragma が必須になる
- 細かい実行方言を踏む可能性がある
-
node_modules/*
の下で ts のトランスパイルを無視する環境で、無理やり有効化する必要がある (next.js)
おわり
ゆくゆくはそのまま rome/deno に載せ替える時代が来たらいいですね。
Discussion
良記事ありがとうございます!参考にさせていただいております。
これは next-transpile-modules を使ってやっていますか?