[TypeScript] CommonJSプロジェクトの型定義で"export default"を使ってはいけない？！　"This expression is not callable"の意味

## はじめに

JavaScriptで実装されているライブラリにおいて、`index.d.ts`で型定義を公開しているものがある。その中で、package.jsonに`"type": "module"`の設定がなくCommonJSの設定になっているプロジェクトにおいて、以下のような`export default`を利用した型定義がいくつか見られた（以下の例は[axios-retry](https://github.com/softonic/axios-retry/tree/master)のindex.d.tsを少しわかりやすくしたもの）。

```ts: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プロジェクトでは以下のようなコンパイルエラーが出てしまう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/a98f245e-cbe3-2d6b-7658-2d72aa019752.png)
```
This expression is not callable
```

今回はこのエラーの原因と解決方法についてみていきたいと思う。

※参考までに、axios-retryではないが、既にマージされた他のリポジトリで遭遇した同じ問題を解消するためのPRを載せておく。

https://github.com/ca98am79/connect-dynamodb/pull/84

## 原因

このエラーの原因は、TypeScriptの仕様がそういう仕様になっているから、というのが結論。詳細は[This expression is not callable for ESM consuming CJS with default export](https://github.com/microsoft/TypeScript/issues/52086)のissueで議論されている。

というわけで型定義の方を修正すべきということになる。

※ちなみに、https://arethetypeswrong.github.io/ というサイトがあり、パッケージの型定義の問題を教えてくれるらしい。

## 解決方法

（少なくとも関数モジュールの場合）結論としては`namespace`をうまく利用して以下のように`export =`を利用するようにする。具体的には、[はじめに](#はじめに)で取り上げたaxios-retryの型定義であれば、以下のようにすればいい。

```ts:index.d.ts
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を使用する](#cjsのプロジェクトではesmoduleinteropを使用する)の章に書いた設定が必要になる）。

以下で型定義についていくつか補足する。

### export = axiosRetry;

もともとの型定義は、恐らく以下のようにexportしただけではデフォルトインポートは利用できず困るので、`export default`を利用して型定義を行ったと思われる（interfaceやtype、今回だと`AxiosRetryConfig`の型定義もexportする必要があり、そうなると`import { axiosRetry } from 'axios-retry';`のようにnamed importを使う必要性が出てくる）。

```ts:index.d.ts
...
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](https://www.typescriptlang.org/docs/handbook/namespaces-and-modules.html#using-namespaces)を参照）。

namespaceを利用した型定義については、[Module: Function](https://www.typescriptlang.org/docs/handbook/declaration-files/templates/module-function-d-ts.html)にも以下のように書かれている通り、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`は使えない](https://www.typescriptlang.org/docs/handbook/declaration-files/templates/module-d-ts.html#default-exports)ので`export =`を使いつつ、interfaceやtypeもexportされるようにnamespaceを利用することになる。

ちなみに、公式のサンプルでは以下のようにnamespace内のinterfaceにexport宣言があるが、これは実は不要である。

```ts:index.d.ts
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

- 参考：[Module: Function](https://www.typescriptlang.org/docs/handbook/declaration-files/templates/module-function-d-ts.html)

## まとめとして

今回はCommonJSのプロジェクトで、型定義で`export default`を利用している場合の問題点と、改善方法についてみてきた。今まで何回かこの`export default`問題でTypeScriptでコンパイルエラーになる事があった。その都度PRを上げたりしているが、この問題はES Moduleでパッケージ公開しているものでは問題にならないので、早くES Moduleだけの世界が来てほしいなとも思った。

※axios-retryについては、以下のようなPRで修正の提案をしてみた。

https://github.com/softonic/axios-retry/pull/241

## おまけ
### CJSのプロジェクトではesModuleInteropを使用する

以下のようにデフォルトインポートを利用して、`export =`で型定義されているライブラリをインポートする場合には、[esModuleInterop](https://www.typescriptlang.org/tsconfig#esModuleInterop)か[allowSyntheticDefaultImports](https://www.typescriptlang.org/tsconfig#allowSyntheticDefaultImports)をtrueにする必要がある。

```ts:hoge.ts
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のコンパイラが暗黙的にデフォルトインポートできるようにデフォルトエクスポートに変換してくれるため。

ちなみに、このフラグを利用しない場合には、以下のように名前空間インポートにすることでも解決することはできる。

```ts
import * as axios from 'axios';
import * as axiosRetry from 'axios-retry';
axiosRetry(axios)
```