import / export /require
Node.jsが出てからというものの、フロントエンドの開発もNode.jsを通して行うことができるようにはなりましたが、実際に動く場所がブラウザとサーバーと事情が異なります。 TypeScriptは最終的にどの場面で使われるか、その用途に適した出力に変えることができます。

かつてのJavaScript

かつてJavaScriptがブラウザでのみ動いていた時代は、モジュール分割と言う考え自体はあったもののそれはあくまでもブラウザ上、さらにはhtmlでの管理となっていました。よく使われていたjQueryというパッケージがあるとすれば、それは次のようにhtmlに書く必要がありました。
1
<script src="https://ajax.googleapis.com/ajax/libs/jquery/x.y.z/jquery.min.js"></script>
Copied!
もしjQueryに依存するパッケージがあるとすれば、 jQuery の宣言より下に書く必要があります。
1
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/x.y.z/jquery-ui.min.js"></script>
Copied!
パッケージが少なければまだしも、増えてくると依存関係が複雑になります。もしも読み込む順番を間違えるとそのhtmlでは動作しなくなるでしょう。

Node.jsが登場してから

npmが登場してから、使いたいパッケージを持ってきてそのまま使うことが主流になりました。

CommonJS

require()

Node.jsでは現在でも主流の他の.jsファイル(TypeScriptでは.tsも)を読み込む機能です。基本は次の構文です。
1
const package1 = require('package1');
Copied!
これは、パッケージのpackage1の内容を定数package1に持ってくることを意味しています。このときpackage1は(組み込みライブラリでなければ)現在のプロジェクトのnode_modulesというディレクトリに存在する必要があります。
自分で作った他の.js, .tsファイルを読み込むこともできます。呼び出すファイルから見た、読み込みたいファイルの位置を相対パスで書きます。たとえ同じ階層にあっても相対パスで書く必要があります。このとき.js, .jsonとTypeScriptなら加えて.tsを省略することができます。TypeScriptでの開発においては最終的にJavaScriptにコンパイルされることを考慮すると書かないほうが無難です。
1
const myPackage = require('./MyPackage');
Copied!
.js.tsと同じ場所に出力するようにしているとTypeScriptにとって同じ名前の読み込ことができるファイルがふたつ存在することになります。このときTypeScriptは.jsを優先して読み込むので注意してください。いくらTypeScriptのコードを変更しても変更が適用されていないようであればこの問題の可能性があります。
また指定したパスがディレクトリで、その中にindex.js(index.ts)があれば、ディレクトリの名前まで書けばindex.js(index.ts)を読み込んでくれます。

module.exports

他のファイルを読む込むためにはそのファイルは何かを出力している必要があります。そのために使うのがこの構文です。
1
// increment.js
2
module.exports = i => i + 1;
Copied!
このような.jsのファイルがあれば同じ階層で読み込みたい時は次のようになります。
1
// index.js
2
const increment = require('./increment');
3
4
console.log(increment(3));
5
// -> 4
Copied!
このとき、読み込んだ内容を受ける定数incrementはこの名前である必要はなく変更が可能です。
このmodule.exportsはひとつのファイルでいくらでも書くことができますが、適用されるのは最後のもののみです。
1
// dayOfWeek.js
2
module.exports = 'Monday';
3
module.exports = 'Tuesday';
4
module.exports = 'Wednesday';
5
module.exports = 'Thursday';
6
module.exports = 'Friday';
7
module.exports = 'Saturday';
8
module.exports = 'Sunday';
Copied!
1
// index.js
2
const day = require('./dayOfWeek');
3
4
console.log(day);
5
// -> 'Sunday'
Copied!

exports

module.exportsだと良くも悪くも出力しているモノの名前を変更できてしまいます。それを避けたい時はこのexportsを使用します。
1
// util.js
2
exports.increment = i => i + 1;
Copied!
読み込み側では次のようになります。
1
// index.js
2
const util = require('./util');
3
4
console.log(util.increment(3));
5
// -> 4
Copied!
分割代入を使うこともできます。
1
// index.js
2
const { increment } = require('./util');
3
4
console.log(increment(3));
5
// -> 4
Copied!
こちらはincrementという名前で使用する必要があります。他のファイルに同じ名前のものがあり、名前を変更する必要がある時は、分割代入のときと同じように名前を変更することができます。
1
// index.js
2
const { increment } = require('./other');
3
const { increment: inc } = require('./util');
4
5
console.log(inc(3));
6
// -> 4
Copied!

ES Module

主にフロントエンド(ブラウザ)で採用されているファイルの読み込み方法です。ES6で追加された機能のため、あまりにも古いブラウザでは動作しません。

import

require()と同じく他の.js, .tsファイルを読み込む機能ですが、require()はファイル内のどこにでも書くことができる一方でimport必ずファイルの一番上に書く必要があります。 なお、書き方が2とおりあります。
1
import * as package1 from 'package1';
2
import package2 from 'package2';
Copied!
使い方に若干差がありますので以下で説明します。

export default

module.exportsに対応するものです。module.exportsと異なりひとつのファイルはひとつのexport defaultしか許されていなく複数書くと動作しません。
1
// increment.js
2
export default i => i + 1;
Copied!
この.jsのファイルは次のようにして読み込みます。
1
// index.js
2
import increment from './increment';
3
4
console.log(increment(3));
5
// -> 4
Copied!
1
// index.js
2
import * as increment from './increment';
3
4
console.log(increment.default(3));
5
// -> 4
Copied!

export

exportsに相当するものです。書き方が2とおりあります。
1
// util.js
2
export const increment = i => i + 1;
Copied!
1
// util.js
2
const increment = i => i + 1;
3
4
export { increment };
Copied!
なお1番目の表記は定数宣言のconstを使っていますがletを使っても読み込み側から定義されているincrementを書き換えることはできません。
次のようにして読み込みます。
1
// index.js
2
import { increment } from './util';
3
4
console.log(increment(3));
5
// -> 4
Copied!
1
// index.js
2
import * as util from './util';
3
4
console.log(util.increment(3));
5
// -> 4
Copied!
1番目の場合のimportで名前を変更するときは、requireのとき(分割代入)と異なりasという表記を使って変更します。
1
// index.js
2
import { increment as inc } from './util';
3
4
console.log(inc(3));
5
// -> 4
Copied!

import()

ES Moduleではimportをファイルの先頭に書く必要があります。これは動的に読み込むファイルを切り替えられないことを意味します。このimport()はその代替手段にあたります。
require()と異なる点としてはimport()はモジュールの読み込みを非同期で行います。つまりPromiseを返します。
1
// index.js
2
import('./util').then(({increment}) => {
3
console.log(increment(3));
4
// -> 4
5
});
Copied!

Node.jsでES Moduleを使う

先述のとおりNode.jsではCommonJSが長く使われていますが、13.2.0でついに正式にES Moduleもサポートされました。
しかしながら、あくまでもNode.jsはCommonJSで動作することが前提なのでES Moduleを使いたい時はすこし準備が必要になります。

.mjs

ES Moduleとして動作させたいJavaScriptのファイルをすべて.mjsの拡張子に変更します。
1
// increment.mjs
2
export const increment = i => i + 1;
Copied!
読み込み側は以下です。
1
// index.mjs
2
import { increment } from './increment.mjs';
3
4
console.log(increment(3));
5
// -> 4
Copied!
importで使うファイルの拡張子が省略できないことに注意してください。

"type": "module"

package.jsonにこの記述を追加するとパッケージ全体がES Moduleをサポートします。
1
{
2
"name": "YYTS",
3
"version": "1.0.0",
4
"main": "index.js",
5
"type": "module",
6
"license": "Apache-2.0"
7
}
Copied!
このようにすることで拡張子を.mjsに変更しなくてもそのまま.jsES Moduleを使えるようになります。なお"type": "module"の省略時は"type": "commonjs"と指定されたとみなされます。これは今までとおりのNode.jsです。
1
// increment.js
2
export const increment = i => i + 1;
Copied!
1
// index.js
2
import { increment } from './increment.js';
3
4
console.log(increment(3));
5
// -> 4
Copied!
.jsではありますが読み込む時は拡張子を省略できなくなることに注意してください。

.cjs

CommonJSで書かれたJavaScriptを読み込みたくなったときはCommonJSで書かれているファイルをすべて.cjsに変更する必要があります。
1
// increment.cjs
2
exports.increment = i => i + 1;
Copied!
読み込み側は以下です。
1
// index.js
2
import { createRequire } from 'module';
3
const require = createRequire(import.meta.url);
4
5
const { increment } = require('./increment.cjs');
6
7
console.log(increment(3));
8
// -> 4
Copied!
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のページに説明がありますのでそちらをご覧ください。

require? import?

ブラウザ用、サーバー用の用途で使い分けてください。ブラウザ用であればES Moduleを、サーバー用であればCommonJSが無難な選択肢になります。どちらでも使えるユニバーサルなパッケージであればDual Packageを目指すのもよいでしょう。

default export? named export?

module.exportsとのexport defaultdefault exportと呼ばれ exportsexportnamed exportと呼ばれています。 どちらも長所と短所があり、たびたび議論になる話題です。どちらか一方を使うように統一するコーディングガイドを持っている企業もあるようですが、どちらかが極端に多いというわけでもないので好みの範疇です。

default export

Pros

    importする時に名前を変えることができる
    そのファイルが他のexportに比べ何をもっとも提供したいのかがわかる

Cons

    エディター、IDEによっては入力補完が効きづらい
    再エクスポートの際に名前をつける必要がある

named export

Pros

    エディター、IDEによる入力補完が効く
    ひとつのファイルから複数exportできる

Cons

    (名前の変更はできるものの)基本的に決まった名前でimportして使う必要がある
    exportしているファイルが名前を変更すると動作しなくなる
ここで挙がっている名前を変えることができるについてはいろいろな意見があります。

ファイルが提供したいもの

たとえばある国の会計ソフトウェアを作っていたとして、その国の消費税が8%だったとします。そのときのあるファイルのexportはこのようになっていました。
1
// taxIncluded.ts
2
export default price => price * 1.08;
Copied!
もちろん呼び出し側はそのまま使うことができます。
1
// index.ts
2
import taxIncluded from './taxIncluded';
3
4
console.log(taxIncluded(100));
5
// -> 108
Copied!
ここで、ある国が消費税を10%に変更したとします。このときこのシステムではtaxIncluded.tsを変更すればこと足ります。
1
// taxIncluded.ts
2
export default price => price * 1.1;
Copied!
この変更をこのファイル以外は知る必要がありませんし、知ることができません。

今回の問題点

システムがある年月日当時の消税率を元に金額の計算を多用するようなものだとこの暗黙の税率変更は問題になります。過去の金額もすべて現在の消費税率である10%で計算されてしまうからです。

named exportだと

named exportであればexportする名称を変更することで呼び出し側の変更を強制させることができます。
1
// taxIncluded.ts
2
export const taxIncludedAsOf2014 = price => price * 1.08;
Copied!
1
// index.ts
2
import { taxIncludedAsOf2014 } from './taxInclude';
3
4
console.log(taxIncludedAsOf2014(100));
5
// -> 108
Copied!
税率が10%に変われば次のようにします。
1
// taxIncluded.ts
2
export const taxIncludedAsOf2019 = price => price * 1.1;
Copied!
1
// index.ts
2
import { taxIncludedAsOf2019 } from './taxIncluded';
3
4
// this is no longer available.
5
// console.log(taxIncludedAsOf2014(100));
6
console.log(taxIncludedAsOf2019(100));
7
// -> 110
Copied!
名前を変更したため、呼び出し元も名前の変更が強制されます。これはたとえasを使って名前を変更していたとしても同じく変更する必要があります。
ロジックが変わったこととそれによる修正を強制したいのであればnamed exportを使う方がわかりやすく、そしてエディター、IDEを通して見つけやすくなる利点があります。逆に、公開するパッケージのようにAPIが一貫して明瞭ならばdefault exportも価値があります。
最終更新 7mo ago