LoginSignup
0
4

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

Last updated at Posted at 2023-08-22

はじめに

JavaScriptで実装されているライブラリにおいて、index.d.tsで型定義を公開しているものがある。その中で、package.jsonに"type": "module"の設定がなくCommonJSの設定になっているプロジェクトにおいて、以下のようなexport defaultを利用した型定義がいくつか見られた(以下の例はaxios-retryのindex.d.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

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の型定義であれば、以下のようにすればいい。

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を使用するの章に書いた設定が必要になる)。

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

export = axiosRetry;

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

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を参照)。

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宣言があるが、これは実は不要である。

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

まとめとして

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

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

おまけ

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

以下のようにデフォルトインポートを利用して、export =で型定義されているライブラリをインポートする場合には、esModuleInteropallowSyntheticDefaultImportsをtrueにする必要がある。

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

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

import * as axios from 'axios';
import * as axiosRetry from 'axios-retry';
axiosRetry(axios)
0
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
4