はじめに
JavaScriptで実装されているライブラリにおいて、index.d.ts
で型定義を公開しているものがある。その中で、package.jsonに"type": "module"
の設定がなくCommonJSの設定になっているプロジェクトにおいて、以下のようなexport default
を利用した型定義がいくつか見られた(以下の例はaxios-retryのindex.d.tsを少しわかりやすくしたもの)。
import * as axios from 'axios'
declare namespace IAxiosRetry {
export interface axiosRetryConfig {
...
}
}
export type axiosRetryConfig = IAxiosRetry.axiosRetryConfig;
declare function axiosRetry(axios: axios.AxiosStatic | axios.AxiosInstance, axiosRetryConfig?: AxiosRetryConfig): void;
export default axiosRetry;
ただこの定義をしてしまうと、CommonJSにコンパイルするTypeScriptプロジェクトでは問題ないが、package.jsonに"type": "module"
の設定があり、ES ModuleのTypeScriptプロジェクトでは以下のようなコンパイルエラーが出てしまう。
This expression is not callable
今回はこのエラーの原因と解決方法についてみていきたいと思う。
※参考までに、axios-retryではないが、既にマージされた他のリポジトリで遭遇した同じ問題を解消するためのPRを載せておく。
原因
このエラーの原因は、TypeScriptの仕様がそういう仕様になっているから、というのが結論。詳細はThis expression is not callable for ESM consuming CJS with default exportのissueで議論されている。
というわけで型定義の方を修正すべきということになる。
※ちなみに、https://arethetypeswrong.github.io/ というサイトがあり、パッケージの型定義の問題を教えてくれるらしい。
解決方法
(少なくとも関数モジュールの場合)結論としてはnamespace
をうまく利用して以下のようにexport =
を利用するようにする。具体的には、はじめにで取り上げたaxios-retryの型定義であれば、以下のようにすればいい。
import * as axios from 'axios';
export = axiosRetry;
declare function axiosRetry(axios: axios.AxiosStatic | axios.AxiosInstance, axiosRetryConfig?: AxiosRetryConfig): void;
declare namespace axiosRetry {
interface AxiosRetryConfig {
...
}
}
上記のようなexport =
を利用した型定義であれば、CommonJS・ES Moduleの両方のプロジェクトで利用可能になる(正確には以下のCJSのプロジェクトではesModuleInteropを使用するの章に書いた設定が必要になる)。
以下で型定義についていくつか補足する。
export = axiosRetry;
もともとの型定義は、恐らく以下のようにexportしただけではデフォルトインポートは利用できず困るので、export default
を利用して型定義を行ったと思われる(interfaceやtype、今回だとAxiosRetryConfig
の型定義もexportする必要があり、そうなるとimport { axiosRetry } from 'axios-retry';
のようにnamed importを使う必要性が出てくる)。
...
export type axiosRetryConfig = IAxiosRetry.axiosRetryConfig;
export declare function axiosRetry(axios: axios.AxiosStatic | axios.AxiosInstance, axiosRetryConfig?: AxiosRetryConfig): void;
ただ、デフォルトインポートのためにこの定義の仕方をしてしまうと、ES Moduleで利用したい開発者にとっては困る。これをCommonJS・ES Moduleの両方で互換をとれるようにするために、export =
に変更している。interfaceやtypeなどをexportしたい時に困るじゃないか、という話があるが、それについては次の章で見るnamesapceを活用すればいい。
※CommonJSのプロジェクトでexport default
を利用するのが全ての元凶になってしまっているので、CommonJSのプロジェクトでは利用すべきではないと考えられる(関数以外のinterfaceやtypeをexportしたい場合は、namespaceにそれらを定義してnamespaceをexportすれば、interfaceやtypeもexportできる)。
declare namespace axiosRetry {...}
まずnamespaceについてだが、TypeScript特有のコード整理の方法で、グローバル名前空間にあるJavaScriptオブジェクトに名前をつけたもの(Using Namespacesを参照)。
namespaceを利用した型定義については、Module: Functionにも以下のように書かれている通り、namespaceに関数の戻り・引数の型定義をしてそれを公開できることがわかるだろう(以下の説明を読むとnamespaceに定義すべきとなっているので、この定義方法が推奨とも読み取れる)。
/*~ If you want to expose types from your module as well, you can
*~ place them in this block. Often you will want to describe the
*~ shape of the return type of the function; that type should
*~ be declared in here, as this example shows.
namespaceは非推奨という話もあるようだが、今回のように関数の型定義をする場合においては非推奨ではなく、むしろ有用だと思う。型が集約されてるDefinitelyTypeの型定義も同じようにnamespaceを利用しているものが多数あり、かつDefinitelyTypeではexport default
は使えないのでexport =
を使いつつ、interfaceやtypeもexportされるようにnamespaceを利用することになる。
ちなみに、公式のサンプルでは以下のようにnamespace内のinterfaceにexport宣言があるが、これは実は不要である。
declare namespace Greeter {
export interface LengthReturnType { // <- export宣言はなくていい
width: number;
height: number;
}
...
}
理由はdtslintの方で以下のようなエラーが出るように、自動的にエクスポートされるため。
'export' keyword is redundant here because all declarations in this module are exported automatically. If you have a good reason to export some declarations and not others, add 'export {}' to the module to shut off automatic exporting.
See: https://github.com/Microsoft/dtslint/blob/master/docs/strict-export-declare-modifiers.md
まとめとして
今回はCommonJSのプロジェクトで、型定義でexport default
を利用している場合の問題点と、改善方法についてみてきた。今まで何回かこのexport default
問題でTypeScriptでコンパイルエラーになる事があった。その都度PRを上げたりしているが、この問題はES Moduleでパッケージ公開しているものでは問題にならないので、早くES Moduleだけの世界が来てほしいなとも思った。
※axios-retryについては、以下のようなPRで修正の提案をしてみた。
おまけ
CJSのプロジェクトではesModuleInteropを使用する
以下のようにデフォルトインポートを利用して、export =
で型定義されているライブラリをインポートする場合には、esModuleInteropかallowSyntheticDefaultImportsをtrueにする必要がある。
import axios from 'axios';
import axiosRetry from 'axios-retry';
axiosRetry(axios);
...
これを設定しない場合、以下のようなコンパイルエラーになる。
This module is declared with 'export =', and can only be used with a default import when using the 'allowSyntheticDefaultImports' flag.
これはexport =
なのでデフォルトでインポートできるモジュールがないために発生するが、esModuleInterop(allowSyntheticDefaultImports)を利用することで、TypeScriptのコンパイラが暗黙的にデフォルトインポートできるようにデフォルトエクスポートに変換してくれるため。
ちなみに、このフラグを利用しない場合には、以下のように名前空間インポートにすることでも解決することはできる。
import * as axios from 'axios';
import * as axiosRetry from 'axios-retry';
axiosRetry(axios)