見出し画像

TypeScriptでexport defaultにする際の注意事項と解決方法


はじめに

こんにちは、SHIFTの開発部門に所属している Katayama です。

TypeScript の型定義において、その型(type hoge = {...})を"export default"(デフォルトエクスポート)している場合に、Node.js にコンパイルして実行した際に、以下のようなエラーが出てしまった。

$ node build/app.js
/home/study/workspace/ts-node-oidc/build/app.js:4
const result = (0, kebabcase_keys_1.default)({ foo_bar: 'baz', nested: { foo_Baz: new Date() } }, { deep: true });
                                            ^

TypeError: (0 , kebabcase_keys_1.default) is not a function
    at Object.<anonymous> (/home/study/workspace/ts-node-oidc/build/app.js:4:45)
    at Module._compile (node:internal/modules/cjs/loader:1196:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1250:10)
    at Module.load (node:internal/modules/cjs/loader:1074:32)
    at Function.Module._load (node:internal/modules/cjs/loader:909:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
    at node:internal/main/run_main_module:22:47

今回は、型定義で"export default"を利用する際の注意についてみていきたいと思う。またおまけとして、型定義が"export = ..."で export されている場合の TypeScript 内でエラーなく利用するための実装についても見ていきたいと思う。

エラーの解決方法

エラーの発生状況について

まず、どのようなプロジェクトの設定・TypeScript の実装をしていた場合に、エラーになったか?を整理する。

package.json の"type"は"commnjs"。

{
    ...
	"type": "commonjs",
    ...
}

tsconfig.json は以下("esModuleInterop"は false)。

{
  "extends": "@tsconfig/node16/tsconfig.json",
  "compilerOptions": {
    "outDir": "./build",
    "declaration": true,
    "sourceMap": true,

    "esModuleInterop": false
  },
  "include": ["srv/**/*.ts"],
  "exclude": ["node_modules", "build"]
}

TypeScript の実装は以下。

import kebabcaseKeys from 'kebabcase-keys';

const result = kebabcaseKeys({ foo_bar: 'baz', nested: { foo_Baz: new Date() } }, { deep: true });
console.log(result);

"kebabcase-keys"の型定義は以下のように"export default"であるとする(@types/kebabcase-keysは"export = ..."になっているが、今回はローカル環境で独自に型定義を行い、"export default function..."になっているとする)。

export default function kebabcaseKeys<T extends ..., OptionsType extends ...>(
    input: T,
    options?: OptionsType,
): KebabCasedProperties<T, ..., ...>;

エラーの詳細について

上記のような状況において、"tsc"コマンドで TypeScript をコンパイルした後、Node.js を実行すると「はじめに」に取り上げていたエラーが発生する。このエラーが発生している Node.js のコードは以下。

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const kebabcase_keys_1 = require("kebabcase-keys");
const result = (0, kebabcase_keys_1.default)({ foo_bar: 'baz', nested: { foo_Baz: new Date() } }, { deep: true });
console.log(result);
//# sourceMappingURL=app.js.map

エラーメッセージからエラーの発生個所は、"(0, kebabcase_keys_1.default)"である事が分かる。

エラーの原因と解決方法

エラーの原因は、TypeScript が CommonJS/AMD/UMD モジュールを ES6 モジュールと同様に扱う事であり、その詳細についてはES Module Interop - esModuleInteropに記載がある。つまりは、モジュールに関する仕様の部分でミスマッチが発生しエラーになっている。

そこでそのミスマッチを埋めてくれるオプションとしてesModuleInteropがあり、これを true に設定すると、このモジュールの仕様におけるミスマッチをうまく解消してくれるようになる。

エラー発生時の tsconfig.json の"esModuleInterop"を true に変更して、TypeScript をコンパイル後、Node.js を実行してみると、エラーなく実行できる事が確認できる。

具体的に"esModuleInterop"により何が変わるか?だが、コンパイル後の Node.js のコードを見ると、以下のように"__importDefault"というヘルパー関数が追加されており、これによりデフォルトインポートを解決できるようになった。

// "esModuleInterop": trueの場合
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const kebabcase_keys_1 = __importDefault(require("kebabcase-keys"));
const result = (0, kebabcase_keys_1.default)({ foo_bar: 'baz', nested: { foo_Baz: new Date() } }, { deep: true });
console.log(result);
//# sourceMappingURL=app.js.map

上記の事から、"export default"を利用して CommonJS にコンパイルする場合には、"esModuleInterop"オプションが必要である事が分かる。そしてこの注意点については、ちゃんとDefault Exportsにも書かれている。

Note that using export default in your .d.ts files requires esModuleInterop: true to work. If you can’t have esModuleInterop: true in your project, such as when you’re submitting a PR to Definitely Typed, you’ll have to use the export= syntax instead. This older syntax is harder to use but works everywhere.
.d.ts ファイルで export default を使用するには esModuleInterop: true が必要です。もし、Definitely Typed に PR を提出する場合など、プロジェクト内で esModuleInterop: true を使用できない場合は、代わりに export=構文を使用する必要があります。この古い構文は使いにくいですが、どこでも使えます。

まとめ

今回は TypeScript の型定義において、"export default"(デフォルトエクスポート)を利用している場合の注意点として、"esModuleInterop"オプションが必要である事をみてきた。公式に書かれている注意事項の意味を実際にエラーを通じて理解を深められたのは良かったのではないかと思っている。

※おまけでは、どこでも利用できる"export = "で型定義を行った時に、TypeScript で利用する方法についてみていきたいと思う。

おまけ

型定義の"export ="について

Default Exportsに書かれていたような方法(型定義を"export = "で行う)場合に、TypeScript で利用する際にはどうするか?について少なくとも 2 つの方法があるので、それぞれ見ていく。

  1. import module = require("module") を利用する

  2. "esModuleInterop": true  を利用する

1. import module = require("module") を利用する

これはexport = and import = require()に書かれている方法で、CommonJS 形式のモジュールを扱うための TypeScript 固有の記法を利用するもの。

実装としては以下のようになる(かなり違和感はあるが…)。

import kebabcaseKeys = require("kebabcase-keys");

const result = kebabcaseKeys(
  { foo_bar: "baz", nested: { foo_Baz: new Date() } },
  { deep: true }
);
console.log(result);

コンパイルされた後のコードは以下。

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const kebabcaseKeys = require("kebabcase-keys");
const result = kebabcaseKeys(
  { foo_bar: "baz", nested: { foo_Baz: new Date() } },
  { deep: true }
);
console.log(result);
//# sourceMappingURL=app.js.map

上記は、package.json の"type": "commonjs"で、tsconfig.json は以下の場合。

  "esModuleInterop": false
  かつ
  "module": "Node16",
  "moduleResolution": "node16"

"moduleResolution": "node"だと、以下のようにコンパイルエラーになる("type": "module"で、"moduleResolution": "node"の場合も同様)。

また、package.json の"type": "module"で、tsconfig.json が以下の場合は、コンパイル後の Node.js は次のようになる。

  "esModuleInterop": false
  かつ
  "module": "Node16",
  "moduleResolution": "node16"
import { createRequire as _createRequire } from "module";
const __require = _createRequire(import.meta.url);
const kebabcaseKeys = __require("kebabcase-keys");
const result = kebabcaseKeys(
  { foo_bar: "baz", nested: { foo_Baz: new Date() } },
  { deep: true }
);
console.log(result);
//# sourceMappingURL=app.js.map

2. "esModuleInterop": true  を利用する

これは上記の"export default"を利用する場合で取り上げた"esModuleInterop"オプションを true にする事で、モジュール間の互換を取れるようにする方法。

実装としては以下のようになる。

import kebabcaseKeys from "kebabcase-keys";

const result = kebabcaseKeys(
  { foo_bar: "baz", nested: { foo_Baz: new Date() } },
  { deep: true }
);
console.log(result);

コンパイルされた後のコードは以下。

"use strict";
var __importDefault =
  (this && this.__importDefault) ||
  function (mod) {
    return mod && mod.__esModule ? mod : { default: mod };
  };
Object.defineProperty(exports, "__esModule", { value: true });
const kebabcase_keys_1 = __importDefault(require("kebabcase-keys"));
const result = (0, kebabcase_keys_1.default)(
  { foo_bar: "baz", nested: { foo_Baz: new Date() } },
  { deep: true }
);
console.log(result);
//# sourceMappingURL=app.js.map

上記は、package.json の"type": "commonjs"で、tsconfig.json が以下の場合。

  "esModuleInterop": true
  かつ
  "module": "Node16",
  "moduleResolution": "node"

上記の設定を "moduleResolution": "node16" にしても、この場合は同じコンパイル結果になる。
また、package.json の"type": "module"で、tsconfig.json が以下の場合、コンパイル後の Node.js は次のようになる。

  "esModuleInterop": true
  かつ
  "module": "Node16",
  "moduleResolution": "node16"
import kebabcaseKeys from "kebabcase-keys";
const result = kebabcaseKeys(
  { foo_bar: "baz", nested: { foo_Baz: new Date() } },
  { deep: true }
);
console.log(result);
//# sourceMappingURL=app.js.map

ただし、"moduleResolution": "node"の場合は、CommomJS にコンパイルされてしまうので以下のようにコンパイルはできても実行時にエラーになる(package.json で ESM のプロジェクト設定になっているので)。

《この公式ブロガーの記事一覧》


執筆者プロフィール:Katayama Yuta
認証認可(SHIFTアカウント)や課金決済のプラットフォーム開発に従事。リードエンジニア。 経歴としては、SaaS ERPパッケージベンダーにて開発を2年経験。 SHIFTでは、GUIテストの自動化やUnitテストの実装などテスト関係の案件に従事したり、DevOpsの一環でCICD導入支援をする案件にも従事。その後現在のプラットフォーム開発に参画。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/