まえがき
TypeScript でモジュール分割しようとした際に困ったことがあり、解決までに時間がかかったので忘れないように記事に残しておきます。TypeScript が得意な方であれば、もしかすると普通に知ってる話かもしれませんが、検索しても情報がなく、私のように躓く人もいそうなので参考になればと思います。
問題の起きたコード
まず初めに、問題の起きたコードを抜粋して載せておきます。問題に関わらない部分は簡略化して記載します。
見ればわかると思いますが、ごく普通のモジュール分割です。myInterface
という型 と myConfig
という値を定義して、index.ts
で再エクスポートするようなありふれたパターンのコードです。
export interface myInterface {
foo: number:
bar: string;
}
export const myConfig = {
lang: 'ja-JP',
timezone: 'Asia/Tokyo',
}
export { myInterface } from './myInterface';
export { myConfig } from './myConfig';
問題発生
特になんの変哲もないコードなのでメイン処理からインポートして動かしてみました。
import { myConfig } from './config';
//メイン処理...
すると、あろうことか、SyntaxError が発生してしましました。😭
Uncaught SyntaxError: The requested module '/src/config/myInterface.ts' does not provide an export named 'myInterface'
しかも、メイン処理では使っていないはずの myInterface
でエラーが発生しています。
調査と不可解な現象
使っていないはずの型でエラーが起きているので、一旦 myConfig
を使用している箇所をコメントアウトして、myInterface
をインポートしてみます。
import { myInterface } from './config';
//メイン処理...(調査用に`myConfig`をコメントアウト)
すると、えっ? あれっ? エラーがでない、、、???
myInterface
で SyntaxError が出てるはずなのに myInterface
を使うとエラーが出ない??? なんで、、、?
原因
これには、すごく悩まされました。なにせ、エラーが起きているはずのコードをインポートするとエラーが起きなくなるのです。
5時間位悩んだ結果、なんとか原因らしきものを見つけました。それは TypeScript の次の仕様によるものだったのです。
型のインポートはトランスパイル時に除去される
この仕様は、TypeScript の isolatedModules の箇所に書かれていました。
何が起きたか
まずは、原因調査のために書き換えた後の main.ts
でエラーが起きなかった理由について。
import { myInterface } from './myConfig'
は型のインポートなので、変数の型チェックのみに使用されて、トランスパイル後のコードからは除去されます。そのため、実行時には index.ts
は読み込まれません。そもそも、問題のコードを読み込んでいないので、エラーも起きません。
次に、書き換え前の main.ts
でエラーが発生した理由について。
書き換え前では、import { myConfig } from './config'
となっていて、これは値のインポートなのでトランスパイル後でも除去されずに残っています。すると、実行時に index.ts
が読み込まれて、各モジュールの再エクスポートを参照して、実装ファイルを読み込む、はずだったのですが、、、
なんと、index.ts
何に書かれている export { myInterface } from './myInterface'
が除去されずに残っているではないですか! しかも、myInterface
自体は型なので、トランスパイル後のコードには存在しません。
つまり、型のインポートは除去されるのですが、型の再エクスポートは除去されずに残ってしまうため SyntaxError が発生してしまったのでした。
解決策
この解決策は、TypeScript 3.8 のドキュメントに書かれていました。
つまり、index.ts
はこうなります。
export type { myInterface } from './myInterface'; // <-typeをつける
export { myConfig } from './myConfig';
export
の後ろに type
を付けることで、型のみのエクスポートであることを明示的に指定して、トランスパイル後の出力結果から除去することができます。
そもそもの話、export { myInterface } from './myInterface'
が自動的に除去されれば問題ない話なのですが、TypeScript(あるいは、JavaScritp)のコードには、エクスポートしているモジュール以外にも副作用を伴うコードが書かれている可能性があります。副作用を伴うコードを勝手に除去してしまうと、コードが実行されないため逆の問題が生じます。そのため、再エクスポート時は自動除去ルールが適用されていないのではと推測されます。
おしまい
今回の問題はかなり悩まされました。構文はあってるように見えるし、検索しても全然情報がでてこないし、使ってない側のコードでエラーが出るし、、、
もし、私のように悩んでいる方がいましたら、参考にしていただければと思います。それでは。