import、export、require
実務でアプリケーションを作る場合、複数のJavaScriptファイルを組み合わせて、ひとつのアプリケーションを成すことが多いです。いわゆるモジュール指向の開発です。ここではJavaScriptとTypeScriptでのモジュールと、モジュール同士を組み合わせるためのimport
、export
、require
について説明します。
スクリプトとモジュール
JavaScriptのファイルは大きく分けて、スクリプトとモジュールに分類されます。スクリプトは普通のJavaScriptファイルです。
スクリプトjs
constfoo = "foo";
スクリプトjs
constfoo = "foo";
モジュールは、import
またはexport
を1つ以上含むJavaScriptファイルを言います。import
は他のモジュールから変数、関数、クラスなどインポートするキーワードです。export
は他のモジュールに変数、関数、クラスなどを公開するためのキーワードです。
モジュールjs
export constfoo = "foo";
モジュールjs
export constfoo = "foo";
したがって、import
やexport
が無かったスクリプトファイルでも、後からimport
やexport
を追加すると、それはモジュールファイルになります。
値の公開と非公開
JavaScriptのモジュールは、明示的にexport
をつけた値だけが公開され、他のモジュールから参照できます。たとえば、次の例のpublicValue
は他のモジュールから利用できます。一方、privateValue
は外部からは利用できません。
js
export constpublicValue = 1;constprivateValue = 2;
js
export constpublicValue = 1;constprivateValue = 2;
JavaScriptのモジュールでは、デフォルトで変数や関数などを非公開になるわけです。Javaなどの他の言語では、モジュール(パッケージ)のメンバーがデフォルトで公開になり、非公開にしたいものにはprivate
修飾子をつける言語があります。そういった言語と比べると、JavaScriptは基本方針が真逆なので注意が必要です。
モジュールは常にstrict mode
モジュールのJavaScriptは常にstrict modeになります。strict modeでは、さまざまな危険なコードの書き方が禁止されます。たとえば、未定義の変数への代入はエラーになります。
js
foo = 1; // 未定義の変数fooへの代入export constbar =foo ;
js
foo = 1; // 未定義の変数fooへの代入export constbar =foo ;
モジュールはimport
時に一度だけ評価される
モジュールのコードが評価されるのは、1回目のimport
のときだけです。2回目以降のimport
では、最初に評価した内容が使われます。言い換えると、モジュールは初回import
でキャッシュされるとも言えますし、モジュールはいわゆるシングルトン(singleton)的なものとも言えます。
たとえば、module.js
というモジュールを3回読み込んだとしても、このmodule.js
が評価されるのは最初の1回目だけです。
module.jsjs
console .log ("モジュールを評価しています");// このログが出力されるのは1回だけexport constvalue = 1;
module.jsjs
console .log ("モジュールを評価しています");// このログが出力されるのは1回だけexport constvalue = 1;
main.jsjs
import "./module.js";import "./module.js";import "./module.js";
main.jsjs
import "./module.js";import "./module.js";import "./module.js";
モジュールの歴史的経緯
かつてのJavaScript
かつてJavaScriptがブラウザでのみ動いていた時代は、モジュール分割と言う考え自体はあったもののそれはあくまでもブラウザ上、さらにはhtml
での管理となっていました。よく使われていたjQuery
というパッケージがあるとすれば、それは次のようにhtml
に書く必要がありました。
<script src="https://ajax.googleapis.com/ajax/libs/jquery/x.y.z/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/x.y.z/jquery.min.js"></script>
もしjQuery
に依存するパッケージがあるとすれば、jQuery
の宣言より下に書く必要があります。
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/x.y.z/jquery-ui.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/x.y.z/jquery-ui.min.js"></script>
パッケージが少なければまだしも、増えてくると依存関係が複雑になります。もしも読み込む順番を間違えるとそのhtml
では動作しなくなるでしょう。
Node.jsが登場してから
npm
が登場してから、使いたいパッケージを持ってきてそのまま使うことが主流になりました。
CommonJS
require()
Node.jsでは現在でも主流の他の.js
ファイル(TypeScriptでは.ts
も)を読み込む機能です。基本は次の構文です。
ts
constpackage1 =require ("package1");
ts
constpackage1 =require ("package1");
これは、パッケージのpackage1
の内容を定数package1
に持ってくることを意味しています。このときpackage1
は(組み込みライブラリでなければ)現在のプロジェクトのnode_modules
というディレクトリに存在する必要があります。
自分で作った他の.js, .ts
ファイルを読み込むこともできます。呼び出すファイルから見た、読み込みたいファイルの位置を相対パスで書きます。たとえ同じ階層にあっても相対パスで書く必要があります。このとき.js, .json
とTypeScriptなら加えて.ts
を省略することができます。TypeScriptでの開発においては最終的にJavaScriptにコンパイルされることを考慮すると書かないほうが無難です。
ts
constmyPackage =require ("./MyPackage");
ts
constmyPackage =require ("./MyPackage");
.js
を.ts
と同じ場所に出力するようにしているとTypeScriptにとって同じ名前の読み込ことができるファイルがふたつ存在することになります。このときTypeScriptは.js
を優先して読み込むので注意してください。いくらTypeScriptのコードを変更しても変更が適用されていないようであればこの問題の可能性があります。
また指定したパスがディレクトリで、その中にindex.js(index.ts)
があれば、ディレクトリの名前まで書けばindex.js(index.ts)
を読み込んでくれます。
module.exports
他のファイルを読む込むためにはそのファイルは何かを出力している必要があります。そのために使うのがこの構文です。
increment.jsts
module .exports = (i ) =>i + 1;
increment.jsts
module .exports = (i ) =>i + 1;
このような.js
のファイルがあれば同じ階層で読み込みたい時は次のようになります。
index.jsts
constincrement =require ("./increment");console .log (increment (3));
index.jsts
constincrement =require ("./increment");console .log (increment (3));
このとき、読み込んだ内容を受ける定数increment
はこの名前である必要はなく変更が可能です。
このmodule.exports
はひとつのファイルでいくらでも書くことができますが、適用されるのは最後のもののみです。
dayOfWeek.jsts
module .exports = "Monday";module .exports = "Tuesday";module .exports = "Wednesday";module .exports = "Thursday";module .exports = "Friday";module .exports = "Saturday";module .exports = "Sunday";
dayOfWeek.jsts
module .exports = "Monday";module .exports = "Tuesday";module .exports = "Wednesday";module .exports = "Thursday";module .exports = "Friday";module .exports = "Saturday";module .exports = "Sunday";
index.jsts
constday =require ("./dayOfWeek");console .log (day );
index.jsts
constday =require ("./dayOfWeek");console .log (day );
exports
module.exports
だと良くも悪くも出力しているものの名前を変更できてしまいます。それを避けたい時はこのexports
を使用します。
util.jsts
exports .increment = (i ) =>i + 1;
util.jsts
exports .increment = (i ) =>i + 1;
読み込み側では次のようになります。
index.jsts
constutil =require ("./util");console .log (util .increment (3));
index.jsts
constutil =require ("./util");console .log (util .increment (3));
分割代入を使うこともできます。
index.jsts
const {increment } =require ("./util");console .log (increment (3));
index.jsts
const {increment } =require ("./util");console .log (increment (3));
こちらはincrement
という名前で使用する必要があります。他のファイルに同じ名前のものがあり、名前を変更する必要がある時は、分割代入のときと同じように名前を変更することができます。
index.jsts
const {increment } =require ("./other");const {increment :inc } =require ("./util");console .log (inc (3));
index.jsts
const {increment } =require ("./other");const {increment :inc } =require ("./util");console .log (inc (3));
ES Module
主にフロントエンド(ブラウザ)で採用されているファイルの読み込み方法です。ES6
で追加された機能のため、あまりにも古いブラウザでは動作しません。
import
require()
と同じく他の.js, .ts
ファイルを読み込む機能ですが、require()
はファイル内のどこにでも書くことができる一方でimport
は必ずファイルの一番上に書く必要があります。
なお、書き方が2とおりあります。
ts
import * as package1 from "package1";import package2 from "package2";
ts
import * as package1 from "package1";import package2 from "package2";
使い方に若干差がありますので以下で説明します。
export default
module.exports
に対応するものです。module.exports
と異なりひとつのファイルはひとつのexport default
しか許されていなく複数書くと動作しません。
increment.jsts
export default (i ) =>i + 1;
increment.jsts
export default (i ) =>i + 1;
この.js
のファイルは次のようにして読み込みます。
index.jsts
importincrement from "./increment";console .log (increment (3));
index.jsts
importincrement from "./increment";console .log (increment (3));
index.jsts
import * asincrement from "./increment";console .log (increment .default (3));
index.jsts
import * asincrement from "./increment";console .log (increment .default (3));
export
exports
に相当するものです。書き方が2とおりあります。
util.jsts
export constincrement = (i ) =>i + 1;
util.jsts
export constincrement = (i ) =>i + 1;
util.jsts
constincrement = (i ) =>i + 1;export {increment };
util.jsts
constincrement = (i ) =>i + 1;export {increment };
なお1番目の表記は定数宣言のconst
を使っていますがlet
を使っても読み込み側から定義されているincrement
を書き換えることはできません。
次のようにして読み込みます。
index.jsts
import {increment } from "./util";console .log (increment (3));
index.jsts
import {increment } from "./util";console .log (increment (3));
index.jsts
import * asutil from "./util";console .log (util .increment (3));
index.jsts
import * asutil from "./util";console .log (util .increment (3));
1番目の場合のimport
で名前を変更するときは、require
のとき(分割代入)と異なりas
という表記を使って変更します。
index.jsts
import {increment asinc } from "./util";console .log (inc (3));
index.jsts
import {increment asinc } from "./util";console .log (inc (3));
import()
ES Module
ではimport
をファイルの先頭に書く必要があります。これは動的に読み込むファイルを切り替えられないことを意味します。このimport()
はその代替手段にあたります。
require()
と異なる点としてはimport()
はモジュールの読み込みを非同期で行います。つまりPromise
を返します。
index.jsts
import("./util").then (({increment }) => {console .log (increment (3));// @log: 4});
index.jsts
import("./util").then (({increment }) => {console .log (increment (3));// @log: 4});
Node.jsでES Module
を使う
先述のとおりNode.jsではCommonJS
が長く使われていますが、13.2.0
でついに正式にES Module
もサポートされました。
しかしながら、あくまでもNode.jsはCommonJS
で動作することが前提なのでES Module
を使いたい時はすこし準備が必要になります。
.mjs
ES Module
として動作させたいJavaScriptのファイルをすべて.mjs
の拡張子に変更します。
increment.mjsts
export constincrement = (i ) =>i + 1;
increment.mjsts
export constincrement = (i ) =>i + 1;
読み込み側は以下です。
index.mjsts
import {increment } from "./increment.mjs";console .log (increment (3));
index.mjsts
import {increment } from "./increment.mjs";console .log (increment (3));
import
で使うファイルの拡張子が省略できないことに注意してください。
"type": "module"
package.json
にこの記述を追加するとパッケージ全体がES Module
をサポートします。
json
{"name": "YYTS","version": "1.0.0","main": "index.js","type": "module","license": "Apache-2.0"}
json
{"name": "YYTS","version": "1.0.0","main": "index.js","type": "module","license": "Apache-2.0"}
このようにすることで拡張子を.mjs
に変更しなくてもそのまま.js
でES Module
を使えるようになります。なお"type": "module"
の省略時は"type": "commonjs"
と指定されたとみなされます。これは今までとおりのNode.jsです。
increment.jsts
export constincrement = (i ) =>i + 1;
increment.jsts
export constincrement = (i ) =>i + 1;
index.jsts
import {increment } from "./increment.js";console .log (increment (3));
index.jsts
import {increment } from "./increment.js";console .log (increment (3));
.js
ではありますが読み込む時は拡張子を省略できなくなることに注意してください。
.cjs
CommonJS
で書かれたJavaScriptを読み込みたくなったときはCommonJS
で書かれているファイルをすべて.cjs
に変更する必要があります。
increment.cjsts
exports .increment = (i ) =>i + 1;
increment.cjsts
exports .increment = (i ) =>i + 1;
読み込み側は次のようになります。
index.jsts
import {createRequire } from "module";constrequire =createRequire (import.meta .url );const {increment } =require ("./increment.cjs");console .log (increment (3));
index.jsts
import {createRequire } from "module";constrequire =createRequire (import.meta .url );const {increment } =require ("./increment.cjs");console .log (increment (3));
ES Module
にはrequire()
がなく、一手間加えて作り出す必要があります。
"type": "module"
の問題点
すべてをES Module
として読み込むこの設定は、多くのパッケージがまだ"type": "module"
に対応していない現状としては非常に使いづらいです。
たとえばlinter
やテストといった各種開発補助のパッケージの設定ファイルを.js
で書いていると動作しなくなってしまいます。かといってこれらを.cjs
に書き換えても、パッケージが設定ファイルの読み込み規則に.cjs
が含んでいなければそれらのパッケージは設定ファイルがないと見なします。そのため"type": "module"
は現段階では扱いづらいものとなっています。
TypeScriptでは
TypeScriptでは一般的にES Module
方式に則った記法で書きます。これはCommonJS
を使用しないというわけではなく、コンパイル時の設定でCommonJS, ES Module
のどちらにも対応した形式で出力できるのであまり問題はありません。ここまでの経緯などはTypeScriptでは意識することがあまりないでしょう。
また、執筆時(2021/01)ではTypeScriptのコンパイルは.js
のみを出力でき.cjs, .mjs
を出力する設定はありません。ブラウザでもサーバーでも使えるJavaScriptを出力したい場合は一手間加える必要があります。
出力の方法に関してはtsconfig.jsonのページに説明がありますのでそちらをご覧ください。
📄️ tsconfig.jsonを設定する
require? import?
ブラウザ用、サーバー用の用途で使い分けてください。ブラウザ用であればES Module
を、サーバー用であればCommonJS
が無難な選択肢になります。どちらでも使えるユニバーサルなパッケージであればDual Packageを目指すのもよいでしょう。
📄️ デュアルパッケージ開発者のためのtsconfig
default export? named export?
module.exports
とexport default
はdefault exportと呼ばれ、exports
とexport
はnamed exportと呼ばれています。どちらも長所と短所があり、たびたび議論になる話題です。どちらか一方を使うように統一するコーディングガイドを持っている企業もあるようですが、どちらかが極端に多いというわけでもないので好みの範疇です。
default export
default exportのPros
import
する時に名前を変えることができる- そのファイルが他の
export
に比べ何をもっとも提供したいのかがわかる
default exportのCons
- エディター、IDEによっては入力補完が効きづらい
- 再エクスポートの際に名前をつける必要がある
named export
named exportのPros
- エディター、IDEによる入力補完が効く
- ひとつのファイルから複数
export
できる
named exportのCons
- (名前の変更はできるものの)基本的に決まった名前で
import
して使う必要がある export
しているファイルが名前を変更すると動作しなくなる
ここで挙がっている名前を変えることができるについてはいろいろな意見があります。
ファイルが提供したいもの
たとえばある国の会計ソフトウェアを作っていたとして、その国の消費税が8%だったとします。そのときのあるファイルのexport
はこのようになっていました。
taxIncluded.tsts
export default (price ) =>price * 1.08;
taxIncluded.tsts
export default (price ) =>price * 1.08;
もちろん呼び出し側はそのまま使うことができます。
index.tsts
importtaxIncluded from "./taxIncluded";console .log (taxIncluded (100));
index.tsts
importtaxIncluded from "./taxIncluded";console .log (taxIncluded (100));
ここで、ある国が消費税を10%に変更したとします。このときこのシステムではtaxIncluded.ts
を変更すればこと足ります。
taxIncluded.tsts
export default (price ) =>price * 1.1;
taxIncluded.tsts
export default (price ) =>price * 1.1;
この変更をこのファイル以外は知る必要がありませんし、知ることができません。
今回の問題点
システムがある年月日当時の消税率を元に金額の計算を多用するようなものだとこの暗黙の税率変更は問題になります。過去の金額もすべて現在の消費税率である10%で計算されてしまうからです。
named exportだと
named exportであればexport
する名称を変更することで呼び出し側の変更を強制させることができます。
taxIncluded.tsts
export consttaxIncludedAsOf2014 = (price ) =>price * 1.08;
taxIncluded.tsts
export consttaxIncludedAsOf2014 = (price ) =>price * 1.08;
index.tsts
import {taxIncludedAsOf2014 } from "./taxIncluded";console .log (taxIncludedAsOf2014 (100));
index.tsts
import {taxIncludedAsOf2014 } from "./taxIncluded";console .log (taxIncludedAsOf2014 (100));
税率が10%に変われば次のようにします。
taxIncluded.tsts
export consttaxIncludedAsOf2019 = (price ) =>price * 1.1;
taxIncluded.tsts
export consttaxIncludedAsOf2019 = (price ) =>price * 1.1;
index.tsts
import {taxIncludedAsOf2019 } from "./taxIncluded";// this is no longer available.// console.log(taxIncludedAsOf2014(100));console .log (taxIncludedAsOf2019 (100));
index.tsts
import {taxIncludedAsOf2019 } from "./taxIncluded";// this is no longer available.// console.log(taxIncludedAsOf2014(100));console .log (taxIncludedAsOf2019 (100));
名前を変更したため、呼び出し元も名前の変更が強制されます。これはたとえas
を使って名前を変更していたとしても同じく変更する必要があります。
ロジックが変わったこととそれによる修正を強制したいのであればnamed exportを使う方がわかりやすく、そしてエディター、IDEを通して見つけやすくなる利点があります。逆に、公開するパッケージのようにAPIが一貫して明瞭ならばdefault exportも価値があります。