import、export、require
実務でアプリケーションを作る場合、複数のJavaScriptファイルを組み合わせて、ひとつのアプリケーションを成すことが多いです。いわゆるモジュール指向の開発です。ここではJavaScriptとTypeScriptでのモジュールと、モジュール同士を組み合わせるためのimport、export、requireについて説明します。
スクリプトとモジュール
JavaScriptのファイルは大きく分けて、スクリプトとモジュールに分類されます。スクリプトは普通のJavaScriptファイルです。
スクリプトjsconstfoo = "foo";
スクリプトjsconstfoo = "foo";
モジュールは、importまたはexportを1つ以上含むJavaScriptファイルを言います。importは他のモジュールから変数、関数、クラスなどインポートするキーワードです。exportは他のモジュールに変数、関数、クラスなどを公開するためのキーワードです。
モジュールjsexport constfoo = "foo";
モジュールjsexport constfoo = "foo";
したがって、importやexportが無かったスクリプトファイルでも、後からimportやexportを追加すると、それはモジュールファイルになります。
値の公開と非公開
JavaScriptのモジュールは、明示的にexportをつけた値だけが公開され、他のモジュールから参照できます。たとえば、次の例のpublicValueは他のモジュールから利用できます。一方、privateValueは外部からは利用できません。
jsexport constpublicValue = 1;constprivateValue = 2;
jsexport constpublicValue = 1;constprivateValue = 2;
JavaScriptのモジュールでは、デフォルトで変数や関数などは非公開になるわけです。Javaなどの他の言語では、モジュール(パッケージ)のメンバーがデフォルトで公開になり、非公開にしたいものにはprivate修飾子をつける言語があります。そういった言語と比べると、JavaScriptは基本方針が真逆なので注意が必要です。
モジュールは常にstrict mode
モジュールのJavaScriptは常にstrict modeになります。strict modeでは、さまざまな危険なコードの書き方が禁止されます。たとえば、未定義の変数への代入はエラーになります。
jsfoo = 1; // 未定義の変数fooへの代入export constbar =foo ;
jsfoo = 1; // 未定義の変数fooへの代入export constbar =foo ;
モジュールはimport時に一度だけ評価される
モジュールのコードが評価されるのは、1回目のimportのときだけです。2回目以降のimportでは、最初に評価した内容が使われます。言い換えると、モジュールは初回importでキャッシュされるとも言えますし、モジュールはいわゆるシングルトン(singleton)的なものとも言えます。
たとえば、module.jsというモジュールを3回読み込んだとしても、このmodule.jsが評価されるのは最初の1回目だけです。
module.jsjsconsole .log ("モジュールを評価しています");// このログが出力されるのは1回だけexport constvalue = 1;
module.jsjsconsole .log ("モジュールを評価しています");// このログが出力されるのは1回だけexport constvalue = 1;
main.jsjsimport "./module.js";import "./module.js";import "./module.js";
main.jsjsimport "./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も)を読み込む機能です。基本は次の構文です。
tsconstpackage1 =require ("package1");
tsconstpackage1 =require ("package1");
これは、パッケージのpackage1の内容を定数package1に持ってくることを意味しています。このときpackage1は(組み込みライブラリでなければ)現在のプロジェクトのnode_modulesというディレクトリに存在する必要があります。
自分で作った他の.js, .tsファイルを読み込むこともできます。呼び出すファイルから見た、読み込みたいファイルの位置を相対パスで書きます。たとえ同じ階層にあっても相対パスで書く必要があります。このとき.js, .jsonとTypeScriptなら加えて.tsを省略することができます。TypeScriptでの開発においては最終的にJavaScriptにコンパイルされることを考慮すると書かないほうが無難です。
tsconstmyPackage =require ("./MyPackage");
tsconstmyPackage =require ("./MyPackage");
.jsを.tsと同じ場所に出力するようにしているとTypeScriptにとって同じ名前の読み込ことができるファイルがふたつ存在することになります。このときTypeScriptは.jsを優先して読み込むので注意してください。いくらTypeScriptのコードを変更しても変更が適用されていないようであればこの問題の可能性があります。
また指定したパスがディレクトリで、その中にindex.js(index.ts)があれば、ディレクトリの名前まで書けばindex.js(index.ts)を読み込んでくれます。
module.exports
他のファイルを読む込むためにはそのファイルは何かを出力している必要があります。そのために使うのがこの構文です。
increment.jstsmodule .exports = (i ) =>i + 1;
increment.jstsmodule .exports = (i ) =>i + 1;
このような.jsのファイルがあれば同じ階層で読み込みたい時は次のようになります。
index.jstsconstincrement =require ("./increment");console .log (increment (3));
index.jstsconstincrement =require ("./increment");console .log (increment (3));
このとき、読み込んだ内容を受ける定数incrementはこの名前である必要はなく変更が可能です。
このmodule.exportsはひとつのファイルでいくらでも書くことができますが、適用されるのは最後のもののみです。
dayOfWeek.jstsmodule .exports = "Monday";module .exports = "Tuesday";module .exports = "Wednesday";module .exports = "Thursday";module .exports = "Friday";module .exports = "Saturday";module .exports = "Sunday";
dayOfWeek.jstsmodule .exports = "Monday";module .exports = "Tuesday";module .exports = "Wednesday";module .exports = "Thursday";module .exports = "Friday";module .exports = "Saturday";module .exports = "Sunday";
index.jstsconstday =require ("./dayOfWeek");console .log (day );
index.jstsconstday =require ("./dayOfWeek");console .log (day );
exports
module.exportsだと良くも悪くも出力しているものの名前を変更できてしまいます。それを避けたい時はこのexportsを使用します。
util.jstsexports .increment = (i ) =>i + 1;
util.jstsexports .increment = (i ) =>i + 1;
読み込み側では次のようになります。
index.jstsconstutil =require ("./util");console .log (util .increment (3));
index.jstsconstutil =require ("./util");console .log (util .increment (3));
分割代入を使うこともできます。
index.jstsconst {increment } =require ("./util");console .log (increment (3));
index.jstsconst {increment } =require ("./util");console .log (increment (3));
こちらはincrementという名前で使用する必要があります。他のファイルに同じ名前のものがあり、名前を変更する必要がある時は、分割代入のときと同じように名前を変更することができます。
index.jstsconst {increment } =require ("./other");const {increment :inc } =require ("./util");console .log (inc (3));
index.jstsconst {increment } =require ("./other");const {increment :inc } =require ("./util");console .log (inc (3));
ES Module
主にフロントエンド(ブラウザ)で採用されているファイルの読み込み方法です。ES6で追加された機能のため、あまりにも古いブラウザでは動作しません。
import
require()と同じく他の.js, .tsファイルを読み込む機能ですが、require()はファイル内のどこにでも書くことができる一方でimportは必ずファイルの一番上に書く必要があります。
なお、書き方が2通りあります。
tsimport * as package1 from "package1";import package2 from "package2";
tsimport * as package1 from "package1";import package2 from "package2";
使い方に若干差がありますので以下で説明します。
export default
module.exportsに対応するものです。module.exportsと異なりひとつのファイルはひとつのexport defaultしか許されていなく複数書くと動作しません。
increment.jstsexport default (i ) =>i + 1;
increment.jstsexport default (i ) =>i + 1;
この.jsのファイルは次のようにして読み込みます。
index.jstsimportincrement from "./increment";console .log (increment (3));
index.jstsimportincrement from "./increment";console .log (increment (3));
index.jstsimport * asincrement from "./increment";console .log (increment .default (3));
index.jstsimport * asincrement from "./increment";console .log (increment .default (3));
export
exportsに相当するものです。書き方が2通りあります。
util.jstsexport constincrement = (i ) =>i + 1;
util.jstsexport constincrement = (i ) =>i + 1;
util.jstsconstincrement = (i ) =>i + 1;export {increment };
util.jstsconstincrement = (i ) =>i + 1;export {increment };
なお1番目の表記は定数宣言のconstを使っていますがletを使っても読み込み側から定義されているincrementを書き換えることはできません。
次のようにして読み込みます。
index.jstsimport {increment } from "./util";console .log (increment (3));
index.jstsimport {increment } from "./util";console .log (increment (3));
index.jstsimport * asutil from "./util";console .log (util .increment (3));
index.jstsimport * asutil from "./util";console .log (util .increment (3));
1番目の場合のimportで名前を変更するときは、requireのとき(分割代入)と異なりasという表記を使って変更します。
index.jstsimport {increment asinc } from "./util";console .log (inc (3));
index.jstsimport {increment asinc } from "./util";console .log (inc (3));
import()
ES Moduleではimportをファイルの先頭に書く必要があります。これは動的に読み込むファイルを切り替えられないことを意味します。このimport()はその代替手段にあたります。
require()と異なる点としてはimport()はモジュールの読み込みを非同期で行います。つまりPromiseを返します。
index.jstsimport("./util").then (({increment }) => {console .log (increment (3));// @log: 4});
index.jstsimport("./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.mjstsexport constincrement = (i ) =>i + 1;
increment.mjstsexport constincrement = (i ) =>i + 1;
読み込み側は以下です。
index.mjstsimport {increment } from "./increment.mjs";console .log (increment (3));
index.mjstsimport {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.jstsexport constincrement = (i ) =>i + 1;
increment.jstsexport constincrement = (i ) =>i + 1;
index.jstsimport {increment } from "./increment.js";console .log (increment (3));
index.jstsimport {increment } from "./increment.js";console .log (increment (3));
.jsではありますが読み込む時は拡張子を省略できなくなることに注意してください。
.cjs
CommonJSで書かれたJavaScriptを読み込みたくなったときはCommonJSで書かれているファイルをすべて.cjsに変更する必要があります。
increment.cjstsexports .increment = (i ) =>i + 1;
increment.cjstsexports .increment = (i ) =>i + 1;
読み込み側は次のようになります。
index.jstsimport {createRequire } from "module";constrequire =createRequire (import.meta .url );const {increment } =require ("./increment.cjs");console .log (increment (3));
index.jstsimport {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を設定する
Node.jsはそれ自身ではTypeScriptをサポートしているわけではないため、TypeScriptの導入をする時はTypeScriptの設定ファイルであるtsconfig.jsonが必要です。
require? import?
ブラウザ用、サーバー用の用途で使い分けてください。ブラウザ用であればES Moduleを、サーバー用であればCommonJSが無難な選択肢になります。どちらでも使えるユニバーサルなパッケージであればDual Packageを目指すのもよいでしょう。
📄️ デュアルパッケージ開発者のためのtsconfig
フロントエンドでもバックエンドでもTypeScriptこれ一本!Universal JSという考えがあります。確かにフロントエンドを動的にしたいのであればほぼ避けて通れないJavaScriptと、バックエンドでも使えるようになったJavaScriptで同じコードを使いまわせれば保守の観点でも異なる言語を触る必要がなくなり、統一言語としての価値が大いにあります。
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.tstsexport default (price ) =>price * 1.08;
taxIncluded.tstsexport default (price ) =>price * 1.08;
もちろん呼び出し側はそのまま使うことができます。
index.tstsimporttaxIncluded from "./taxIncluded";console .log (taxIncluded (100));
index.tstsimporttaxIncluded from "./taxIncluded";console .log (taxIncluded (100));
ここで、ある国が消費税を10%に変更したとします。このときこのシステムではtaxIncluded.tsを変更すればこと足ります。
taxIncluded.tstsexport default (price ) =>price * 1.1;
taxIncluded.tstsexport default (price ) =>price * 1.1;
この変更をこのファイル以外は知る必要がありませんし、知ることができません。
今回の問題点
システムがある年月日当時の消税率を元に金額の計算を多用するようなものだとこの暗黙の税率変更は問題になります。過去の金額もすべて現在の消費税率である10%で計算されてしまうからです。
named exportだと
named exportであればexportする名称を変更することで呼び出し側の変更を強制させることができます。
taxIncluded.tstsexport consttaxIncludedAsOf2014 = (price ) =>price * 1.08;
taxIncluded.tstsexport consttaxIncludedAsOf2014 = (price ) =>price * 1.08;
index.tstsimport {taxIncludedAsOf2014 } from "./taxIncluded";console .log (taxIncludedAsOf2014 (100));
index.tstsimport {taxIncludedAsOf2014 } from "./taxIncluded";console .log (taxIncludedAsOf2014 (100));
税率が10%に変われば次のようにします。
taxIncluded.tstsexport consttaxIncludedAsOf2019 = (price ) =>price * 1.1;
taxIncluded.tstsexport consttaxIncludedAsOf2019 = (price ) =>price * 1.1;
index.tstsimport {taxIncludedAsOf2019 } from "./taxIncluded";// this is no longer available.// console.log(taxIncludedAsOf2014(100));console .log (taxIncludedAsOf2019 (100));
index.tstsimport {taxIncludedAsOf2019 } from "./taxIncluded";// this is no longer available.// console.log(taxIncludedAsOf2014(100));console .log (taxIncludedAsOf2019 (100));
名前を変更したため、呼び出し元も名前の変更が強制されます。これはたとえasを使って名前を変更していたとしても同じく変更する必要があります。
ロジックが変わったこととそれによる修正を強制したいのであればnamed exportを使う方がわかりやすく、そしてエディター、IDEを通して見つけやすくなる利点があります。逆に、公開するパッケージのようにAPIが一貫して明瞭ならばdefault exportも価値があります。