TypeScript Express で API を開発していて cors
を使おうとした際にエラーになってハマったのでメモ
"TypeError: cors is not a function"
次のようなコードでエラーになった
import * as cors from 'cors'; const app = express(); const router = express.Router(); app.use(cors());
=> TypeError: cors is not a function
cors
が function ではない???? なんでや工藤!!!!
CommonJS 形式のモジュールの import 方法に問題があった
import * as cors from 'cors';
これ。
お作法通り import cors from 'cors'
だと VSCode の赤線が出て ESLint が下記のようなメッセージを出していた
Module '"/XXX/node_modules/@types/cors/index"' can only be default-imported using the 'esModuleInterop' flagts(1259) index.d.ts(58, 1): This module is declared with using 'export =', and can only be used with a default import when using the 'esModuleInterop' flag.
import * as cors from 'cors'
にすればこのメッセージが消えたので、安易にそうしてしまっていたのが問題だった。
結論: CommonJS なモジュールをインポートする時 default import (import Module from 'module'
) と namespace import を使った default import (import * as Module from 'module'
) の挙動には違いがある!
cors (v2.8.5
) は module.exports
でエクスポートされていた
(function () { // 略 // can pass either an options hash, an options delegate, or nothing module.exports = middlewareWrapper; }());
cf. cors/index.js at master · expressjs/cors · GitHub
module.exports
されているモジュールを * as
(namespace import) でデフォルトインポートした場合は .default
プロパティを通じてでしかアクセスができない
TypeScriptのデフォルトではCommonJSなどのモジュールをES6と同様に扱うため、
- a namespace import like
import * as moment from "moment"
acts the same asconst moment = require("moment")
- a default import like
import moment from "moment"
acts the same asconst moment = require("moment").default
cf.
TypeScriptのesModuleinteropフラグを設定してCommonJSモジュールを実行可能とする | DevelopersIO
TypeScript: TSConfig Reference - Docs on every TSConfig option
なので namespace import を使った デフォルトインポートをした場合は下記の書き方らな問題がなかった
import * as cors from 'cors'; app(cors.default());
ただし上記の書き方は見慣れないので、 default import を使ってもエラーを表示しないようにするのが健全
tsconfig の設定で default import を許容するのが健全
esModuleInterop
を有効にすると CommonJS なモジュールを default import で扱えるようになる
tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
// ...
+ "esModuleInterop": true,
}
}
👇 default import してもエラーが表示されなくなる
import cors from 'cors'; app(cors);
📝 esModuleInterop
と allowSyntheticDefaultImports
default import の型の解決自体は allowSyntheticDefaultImports
が担っていて、 esModuleInterop
はトランスパイル時のコードの変換も含んでいる
esModuleInterop
を有効にしたら自動的に allowSyntheticDefaultImports
も有効になる
もともとビルドのエラーが発生してないのであれば allowSyntheticDefaultImports
を有効にするだけでも良いと思う
allowSyntheticDefaultImports
When set to true, allowSyntheticDefaultImports allows you to write an import like:import React from "react";
instead of:import * as React from "react";
This flag does not affect the JavaScript emitted by TypeScript, it only for the type checking. This option brings the behavior of TypeScript in-line with Babel, where extra code is emitted to make using a default export of a module more ergonomic.esModuleInterop
Enabling esModuleInterop will also enable allowSyntheticDefaultImports.
cf.
TypeScript: TSConfig Reference - Docs on every TSConfig option
TypeScript の esModuleInterop フラグについて - 30歳からのプログラミング
CommonJS と ESModule
CommonJS
module.exports
, exports
したモジュールを require()
でインポートする形式
Node.js では今も主流の方法
ES Module (ESM)
export default
, export
したモジュールを import
でインポートする形式
主にブラウザのフロントエンドで採用されている方法
フロントエンド開発をしている場合 CommonJS で書かれたライブラリを ES Module でインポートするケースがあるのでこの場合の挙動を抑えておくと良さそう。
CommonJS の export
module.exports
ES Module で言うところの export default
に近い
// increment.js module.exports = (i) => i + 1;
default import
🙆
import increment from './increment'; increment(1); // => 2 // 好きな名前で import できる import Foo from './increment'; Foo(1); // => 2
* as
使った namespace import
🙅♀️
import * as increment from '../libs/increment'; increment(1); // => TypeError: increment is not a function
🙆
// module.export されたモジュールは default という名前で import されている import * as increment from './increment'; increment.default(1); // => 2 // * as も好きな名前でインポートできる import * as Foo from './increment'; Foo.default(1); // => 2
📝 module.exports = { default: Module, key: Module }
レアな書き方だと思うけど、default
キーを使ったオブジェクトが module.exports
されているモジュールを ESM でインポートした場合は .default
を通じてでないと Module
にアクセスができない
default
キー以外のプロパティは exports
されたものと同じ扱いになる
default import
// increment.js module.exports = { default: (x) => x + 1, name: 'increment', };
🙅♀️
import increment from "./increment"; increment(2); // => TypeError: (0 , _incrementDefault.default) is not a function
module.exports = { default: Module }
の時はオブジェクトとして export されているので increment
はオブジェクトになるのでそのまま関数を呼び出すことは当然できない
🙆
import increment from "./increment"; increment.default(2); // => 3 increment.name; // => "increment" // 好きな名前で import できる import Bar from './increment'; Bar.default(3); // => 4 Bar.name; // => "increment"
⚠ named import できるが default
は特殊な名前なので注意が必要
🙅♀️
import { default, name } from './increment'; name; // => "increment" default(1); // => SyntaxError: Unexpected token
🙅♀️
import { default as Alias } from './increment' Alias(1); // => TypeError: (0 , _incrementDefault.default) is not a function console.log(Alias); // => { default: ƒ default(), name: "increment" }
🙆
import { default as Alias } from './increment' Alias.default(1); // => 2 Alias.name; // => "increment"
default
プロパティは特殊な名前なのでそのまま named import できない。{ default as Alias }
を使う必要があるが、Alias はdefault
で定義された値ではなくモジュール全体をデフォルト import した際のオブジェクトになるmodule.exports = { default: Module }
されているモジュールは as を使ってインポートしても default import したときと同じになる
* as
使った namespace import
// increment.js module.exports = { default: (x) => x + 1, name: 'increment', };
🙅♀️
import * as Alias from './increment' // default 以外のプロパティにはアクセスできる Alias.name; // "increment" Alias(1); // => Alias is not a function Alias.default(1); // => Alias.default is not a function console.log(Alias); // => { name: increment", default: { default: ƒ default(), name: "increment" } }
* as
import した際は .default
でインポートされたオブジェクトにアクセスできていたので Alias.default
が module.exports
されたオブジェクトになっている
🙆
import * as Alias from './increment' Alias.name; // "increment" Alias.default.default(1); // => 2 Alias.default.name; // "increment"
module.exports = { default: Module }
のように default
キーが使われたオブジェクトが module.exports
されているモジュールを * as
import すると Module
にアクセスするには default.default
としなければならなくなる
exports.Module
ES Module で言うところの export
に近い
// decrement.js exports.decrement = (x) => x - 1;
default import
🙆
import Alias from "./decrement" console.log(Alias) // => { decrement: f() } Alias.decrement(2); // => 1
default import したオブジェクト内に exports
したモジュールが含まれている
* as
使った namespace import
🙆
import * as Alias from "./decrement" console.log(Alias) // => { decrement: f(), default: { decrement: ƒ () } } Alias.decrement(2); // => 1 Alias.default.decrement(3); // => 2
* as
を使った namespace import だとインポートしたオブジェクト内に default
というプロパティが含まれるが exports
されたモジュールには default import と同じ方法でアクセスができる
exports.Module = X
は module.exports = { Module: X }
と同じ
exports
が使われている CommonJS のモジュールを使用する際は named import を使うのが良い
exports
されたモジュールは * as
を使うかどうかに関わらず default import せずに named import をするのが分かりやすい
import { decrement } from "./decrement"; decrement(2); // => 1
所感
"ふんいき" でコード書いてると稀によくある。。。
少しだけ理解が進んだので良かった。
ちゃんと理解していくのが真の近道ですね…
おわり。
[参考]