• 作成:
  • 更新:

JavaScriptのexportはexport default以外禁止にしてしまった方が楽になる

追記: 2020-12-11

別にそうでもないなと考えを変えました.

なぜ default export を使うべきではないのか? - LINE ENGINEERING

には納得できる論も多いですし, この時やっていたプロジェクトのモジュールがつらいのは単に命名がまずかったからです.

概要

相当遅ればせながらJavaScriptのモジュールについて調べて, 自分なりの付き合い方をまとめました.

結論: export defaultのみを使おう. 他のexportはやめよう.

JavaScriptコードのimportがつらい状況になっている

本質的な問題ではないですが, 以下のようなコードがあってつらい.

import {
  actionFoo,
  actionBar,
  actionBaz,
  actionQux,
} from '../redux/action';

本物はもっとひどく, 名前付きimportの名前が50行程度あります. これをまともな形式に修正したい. 数個ならともかく, 50個もimportするならそれを列挙するのはつらすぎる.

JavaScript(Babel, ES2015)のモジュール機構に詳しくないのでどう修正すれば良いのかぱっと見わからない. どれが利用されるようになるかわからない状況なのでまともに学習していなかったので, JavaScriptのモジュールを未だ把握していないので, 基礎的な知識がない. 見に行きます.

import - JavaScript | MDNを見る限り, 修飾無しで全てのexportimportする方法は無いように見えます.

いやまだ諦めたくないです.

ES6 — modules – ECMAScript 2015 – Medium によると, import * from 'modules';という書き方が出来るように見えるのですが, 手元で確かめてみると構文エラーになります. 規格を見ても, そんな構文は無いようです. 古い提案ですかね?

修正するのは無理っぽいですね. つらい.

JavaScriptで名前で空間構造を分けるのはアンチパターン

どうすれば良かったのか.

actionを纏めるならActionオブジェクトみたいなものを作って, そのメンバとしてそれぞれのアクションを参照するようにするべきなんですよね.

export default class Actionに静的メソッドや静的プロパティとしてそれぞれのaction定数を定義するのが良い. そうしたらAction.foo()のような構文になります.

もしくはせめて, それぞれの関数をactionという名前をつけずにexport functionするのが良い. そしたらimportする側ではActionオブジェクトを作って結果的にAction.foo()のような構文になります.

camelCaseでもSNAKE_CASEのどちらでもJavaScriptで名前で空間構造を分けるのはアンチパターンです. JavaScriptにはせっかくオブジェクトがあるのですからオブジェクトで名前を分けましょう. C++の名前空間などとは違って構文もスッキリとしていますし.

JavaScriptのimport/export(モジュール)がつらすぎる

この件にぶちあたって初めてJavaScript(ES2015)のモジュールを真面目に調べてやっとまともに学習しました. JavaScriptのモジュールはやはりつらいと再確認しました.

他の言語ではモジュール名に相当するものがJavaScriptではファイル名です.

そして, JavaScriptは他の言語と違ってモジュール名と識別子の両方をimportする側が決めなければいけません.

Haskellではimport Data.Ratioのようにimportで指定するのはモジュール名だけで十分です. デフォルトではグローバルな名前空間にデータ型や関数などの識別子が展開されます. グローバルな名前空間に展開されるので定番のモジュール以外はよく衝突しますが, 衝突したときはasや名前付きimportで衝突を回避することができます. 衝突した場合はasで識別子をimportする側が決めなければいけませんが, 衝突しない場合は問題ありません.

Javaではimport java.lang.Math;のようにimportで指定するのはパッケージ名だけで十分です. Javaでは1つのパッケージで1つのクラスだけが公開できてそれが識別子となり, メソッドやプロパティなどはクラス以下に存在するので, 衝突することはほぼありません. 衝突したら上位のパッケージ名を指定して回避することが出来ます. グローバルな名前空間に展開したいときのみimport staticを使うことが出来ます. import static java.lang.Math.*;すればMath以下の識別子が全てグローバルな名前空間に展開されて楽ですね.

JavaScriptでimportする時はファイル名と識別子の両方を指定する必要があります. この時点でつらい.

モジュールのオブジェクトがexport defaultのみを使ってexportされていた場合, Javaのような形式になります. import Foo from "foo"のような形式でimportを行った場合, 識別子はFoo以下に展開されます.

モジュールが複数exportを行っていた場合, import * as Foo from "foo"と書くことでFoo以下に関数をまとめることが出来ます.

グローバルな名前空間に識別子を展開したい場合, 名前付きimportを使うことでのみそれが可能です. その際ワイルドカードなどを使って全て展開することは不可能です. よってグローバルな名前空間で使うことを想定した関数群を含むモジュールをimportする際, 使う識別子を全て列挙するという地獄が発生します.

要約すると,

  • Haskellではデフォルトで全ての識別子をグローバル名前空間に展開できて, 衝突した場合のみ識別子に別名を付けてimport出来る
  • Javaではデフォルトではクラス名の識別子しかグローバル名前空間に展開できない, import staticを使えばグローバル名前空間に展開できる
  • JavaScriptではデフォルトでimport側が識別子に別名を付けないとimportできない, グローバル名前空間に展開する時は一つ一つ識別子を列挙する必要がある

ちょっと辛すぎますね… 動的型付け言語だからというのを考えてもつらいです.

あまりつらくならずにJavaScriptのモジュールと付き合う方法

exportする側は, export defaultのみを使いましょう.

export default classを使えばJava風になってそこまでつらくないです.

単一の関数のみをexportしたい場合はexport default functionを使いましょう. importする側の識別子も小文字初めで付けることが出来るので, 問題ありません.

ただ, export defaultする際もclassfunctionに名前はちゃんと付けましょう. それを参照すれば, import側が識別子に悩むことが無くなるためです. 名前をちゃんと付ければ, 静的解析ツールやIDEの助けにもなります.

importする側はimport Foo from "foo"の形式のみを使いましょう.

1つのオブジェクトに識別子を纏めることで, 混乱を避けることが出来ます. 複数の識別子をexportしたい場合は, classにまとめたりobjectに纏めることでexport defaultにしましょう.

複数の関数をオブジェクトにまとめずにexportしたい時は, 本当にそれが必要なのか, それで設計が破綻しないのかちゃんと考えましょう.

複数の関数をexportしても, importする側はどうせimport * as Foo from "foo"のように1つのオブジェクトにまとめます. それなら初めからexportする側がオブジェクトに纏めたほうが, importする側は混乱しません.

グローバル名前空間に展開する関数を複数exportしたい? 本当にそれは必要なことなのですか? 関数名に共通するプレフィクスを取り出してclassに纏めてしまうことで, 回避できませんか?

必要だとしても, そうするとimportする側は一つ一つグローバル名前空間に展開する識別子を列挙する必要があって地獄が発生しますが, 本当にそれで良いですか? 今は数個だとしても, 識別子が増えてきて破綻しませんか?

どうしてもimportする方でグローバル名前空間で識別子を使いたいのならば, それだけ変数束縛してもらいましょう.

とにかくexportexport default以外禁止にしてしまえば, 識別子をimport側にも書かないといけないこと以外は平穏にJavaScriptのモジュールと付き合っていくことが可能です.

Axel Rauschmayer博士もdefault exportを推奨しています. ECMAScript 6 modules: the final syntax

勿論既存のコードとは折り合いを付けなければいけませんが…

つらい

既にとあるプロジェクトの大部分のコードが大量名前付きimportする設計に依存しているので今更修正できない. とてもつらい.

import * as Action from '../redux/action'とするのも不可です. 既存のコードをぶち壊しますし, Action.actionFoo()となって名前が重複するからです.