コラム:2021年5月現在のTypeScriptとES Moduleの状況

バンドラーを使わずにTypeScriptでES Moduleとしてコードをコンパイルする際に注意すること

 本連載のスピネルライブラリでやっているように、バンドラーを使わずにTypeScriptでコードをES Moduleとしてコンパイルするには、tsconfig.jsのcompilerOptions.modulees2015またはESNextを指定する必要があります。あるいはtscコマンドを直接使ってコンパイルする際は--module es2015または--module ESNextを指定します。

 そのうえで、TypeScriptソースコード上での他ファイルのimportはパスのファイル名に必ず.jsの拡張子を付けなければなりません。

import esModuleA from "./esModuleA" // ダメ。実行時にエラー。 import esModuleA from "./esModuleA.js" // OK.

背景となる「ややこしい」お話

 なぜこのようなことになっているかというと、現時点ではJavaScriptのパッケージ仕様であるES ModuleとTypeScriptのimport仕様の間で連携上の問題があるためです。

ES Moduleの仕様では外部JSファイルのimport時には拡張子を省略することができません。一方のTypeScriptでは、本来の作法としてはimportの際に拡張子をつけないことになっています。

import esModuleA from "./esModuleA"

実は上記の拡張子を付けない書き方が本来のTypeScriptの作法です。この本来のやり方では、TypeScriptの他にWebpackなどのバンドラーと呼ばれる変換ソリューションを併用することになります。

バンドラーはコンパイル後のコードを1つのファイルに変換するのですが、その際にこれらのTypeScriptファイル間のimport関係をうまく解決し、開発者が指定するいずれかのモジュール形式(ESModule以外にもCommonJSやUMDなど他のモジュール形式が存在します)に対応した.jsファイルに変換されます。

ここまで読んで、ややこしいと思いましたか? 実際、開発者ですら苦労が絶えないほどややこしい状況なのが現在のJavaScript界隈のパッケージ運用状況です。

本連載のスピネルライブラリでバンドラーの利用を見送ったのは、このあたりの知識や設定があまりに複雑で、覚えたとしてもいずれまた状況が変わってしまう可能性が高いため、避けた方が無難と判断したためです。

そのため、スピネルライブラリではTypeScriptでES Moduleとして各.tsファイルをコンパイルし、それらをES Moduleとしてブラウザから直接ロードするシンプルな形式を採用しました。

今はまだ主流の方法ではありませんが、いずれこうした素朴なやり方も現実的になってくるかもしれません。

TypeScriptでバンドラーを使わずにES Moduleとしてソースコードをコンパイルする

前述の通り、TypeScriptの通常のimport文では拡張子をつけません。

import esModuleA from "./esModuleA"

試しに.tsをつけると、コンパイルエラーになります。

import esModuleA from "./esModuleA.ts" // コンパイルエラー

しかし、奇妙なことに仕様として.jsをつけることは許容されています。インポートするファイルは.tsファイルなのですが、コンパイルが通るのです。

import esModuleA from "./esModuleA.js" // コンパイルが通る!

少し不安を覚える方法ですが、バンドラーを使わずにTypeScript(tscコマンド)のみでES Module出力したものを問題なく動作させる現行唯一の方法です。最近では運用方法として事実上のコンセンサスになりつつあります。

I'm having problems with ESM and TypeScript 

試しに、拡張子なしの本来のimport文でES Moduleとしてコンパイルすると、コンパイルは通りますがブラウザがES Moduleとしてロードする際にエラーが発生します。.js拡張子がないためです。

この問題の経緯はどうもTypeScriptのモジュール解決の方式がNode.jsのやり方(tsconfig設定のmoduleResove: "node"に相当)に固執しているところに起因しているようです。

参考記事:最近のTypeScriptのES Modules対応事情 
こちらの記事によると、

TypeScriptはJavaScriptのスーパーセットという位置づけなので、JavaScriptで有効なコードはtscで変換しないという固いポリシーがあり...

ここではESModuleではなくCommonJSとして有効なコード(moduleResove: "node"のため)、という意味でしょうね。

当然、この辺りのESModuleとの相互運用性の不便さを指摘するIssueがTypeScriptのGithubプロジェクトで提起されていますが、2021/05時点でもTypeScript公式としては直す気があまりないようです。
https://github.com/Microsoft/TypeScript/issues/13422 
https://github.com/microsoft/TypeScript/issues/16577 

各バンドラーのES Module出力対応に期待

もっとも、WebpackなどのいくつかのバンドラーではES Module出力に対応しようという動きが見られます。

output.library.type属性 (Webpack 5)

関連Issue「support output.libraryTarget: 'module' · Issue #2933 · webpack/webpack」 

実際に試したところ、2021/05時点ではまだ実装が完全でなく実用に耐えませんでした。しかしいずれ十分な対応品質になれば、TypeScriptコーディングにおいて通常の拡張子なしでimport文の記述できるようになります。Webpackの知識や設定は必要になりますが、Webpackが提供する豊富な機能やプラグインをES Module出力の際も利用できるようになります。

余談ですが、Node.js作者によるNodeの設計改良版であるdeno はES Moduleを前提としています。WebpackなどのバンドラーのES Module出力対応が安定すれば、Nodeとdenoの両対応も容易になるでしょう。