uhyo/blog

アンサー: named exportは有害なのか

2021年9月9日 公開

こんにちは。ここ数日は、以下の記事が話題になりました。

「named exportは有害」という主張はこれまで常識と思われていたこととは異なるため、界隈のエンジニアからは否定的・懐疑的な意見が見られます。実際、筆者もnamed exportが有害であるとは1ミリグラムも思っていません。

しかし、自分と異なる意見は当然に下等・幼稚なものであるというのは筆者が最も嫌う考え方ですから、このような異なる意見を分析・理解する必要があると思い、アンサー記事という形でまとめました。具体的には、異なる意見に達する理由としては前提が異なることと論理が異なることが主に挙げられます。前提が異なることが分かれば、自分と異なる意見に至った理由を理解でき、場合によっては取り入れることもできます。論理が違うのであれば、それは瑕疵であり指摘しなければいけません。

なお、そもそも「named export/default exportの違いは些細なものでありこだわるべきではない」といった視座の高い意見も見られますが、考察を避けるためにそのような意見を発することには筆者はそれには賛同しません。確かに細かいことではありますが、こういった細かい考察の積み重ねがクオリティの高いコードを書く能力につながると考えているからです。人が言っているものを無条件に受け入れる態度は必然性に欠けるので高いクオリティに繋がりにくいし、ときにこのような矛盾に直面してしまいます。自分の前提を持ち自分で考察して決めるか、もしくは各主張の前提を理解し自分の前提と合致するものを取り入れるというのが理想的な状態だと考えます。論理的に考えてどちらでも良いという結論に至ってから初めて、こだわるべきではないと言うことができるのです。

なお、この記事は元記事を読みながら適宜感想を書くというスタイルで書いたので、あまりまとまりの無いものになっています。結論は末尾に書いてあるのでそこだけ読むのも良いでしょう。

分析 - 前提を理解しながら

まず、冒頭で紹介した記事(以下では元記事と呼びます)と自分の考えが違う理由を理解するには、元記事がどのような前提の上に立っているかを理解する必要があります。そこで、ここでは元記事に現れている前提を抜き出し、それに対する筆者の見解を明らかにします。「結局どちらが正しいの?」と思っている方は、異なる前提のうちどちらが自分の立場に近いかを考えることで、自分にとってどちらが正しいのかを理解することができるでしょう。

モジュールのブラックボックス性

元記事において前提となる考え方のひとつは、モジュールはブラックボックスであるというものです。元記事から引用すると:

基本的にモジュールというのは、importする側にとってはブラックボックスに見えると思っておいた方がいいです。少なくとも理想論としては、そういうつもりで引数に名前をつけ、ドキュメンテーションコメントを書き、そのモジュールを使ううえで困らないだけの情報をIntelliSenseのツールチップ経由で提供しなければなりません。

筆者の考えでは、「モジュールはブラックボックスである」という主張自体にはある程度同意できます。より具体的に言えば、モジュールの内部実装について外から知ることができるべきではありません。モジュールからエクスポートされる関数(など)の動作がどうであるかが外から見たときに重要なのであって、その関数がどのように実装されており、モジュール内にどんな補助関数(エクスポートされていない)があるのかは知ったことではありません。

ただし、元記事において「モジュールはブラックボックスである」というのはそのような意味で使われていないようです。元記事には次のようにあります。

そういうベストプラクティスを行うための心構えに対しても、named exportは真っ向から反しています。IntelliSenseのサジェストを使ってわざわざ探らなければならないような何かを、named exportの裏に「隠す」のですから。

つまり、元記事の考え方においては「モジュールが何をnamed exportするのか」ということは内部実装の一部であり、default exportのみが外から内部実装を知らずに使えるインターフェースであるということです。理想的にはdefault exportのみがモジュール間のコミュニケーションに使える合意されたインターフェースであり、モジュールがブラックボックスであることの要件として、モジュールが何をexportしているのか知らなくても使えることが含まれています。

これは筆者の頭にある前提とは食い違っており、筆者がこの記事と意見を異にする一因です。この点について、元記事と同じ前提を共有できる人は多くないのではないでしょうか。

ちなみに、筆者の考えとしては、「モジュールが何をエクスポートするのか」はモジュールのインターフェースに含まれるものであり、モジュール間コミュニケーションの正式な手段として認められるというものです。その根拠として、JavaScriptではモジュールが何をエクスポートするのかは静的解析可能であり、またdefault exportも「defaultという名前でエクスポートする/defaultという名前をインポートするために便利なsyntax sugarがある」という点でのみ他のnamed exportと区別されるものであり、named exportと本質的な違いは無いと考えていることが挙げられます。

ただし、だからと言って一つのモジュールから100個も200個もnamed exportすべきでないということは、筆者も同意するところです。結局は設計の問題です(これは元記事でも述べられていることであり、“すべてをnamed exportにした場合でも適切に設計すれば問題ないかもしれませんが”とされています)。元記事では最も極端な方針として「2個以上exportすることは問題である」を採用し、常に1個ならば名前をつける必要がないためdefault exportのみを使えばいいという発想になったと考えられます。また、そこから一歩進んで、「named exportという機能を提供することは2個以上エクスポートする悪い設計を促進するから有害である」という主張につながっていると解釈できます。

カプセル化の手段としてのモジュールについて

筆者は、JavaScriptのモジュールをカプセル化の手段として捉えています。つまり、モジュール内のトップレベルで定義された変数はモジュールスコープに属する変数となり、そのモジュール内のコードからしか参照することができません。

この特徴を最大限利用する場合、1つしかexportしないという制限では立ちいかなくなる(制限を回避したとしても、本質的な意味のないworkaroundになる)と考えています。複数の機能が内部で同じものに依存し、かつそれは外から見えなくなっていることが設計上望ましい場合があります。具体的には、何かのキャッシュをWeakMapで管理したりとか、あるいはRecoilでステートツリーを構築したりとかです。

このような場合、named exportを禁止したところで、より回りくどい代替手段が使われるだけで本質的な改善には繋がりません。

ただし、あくまでそういったユースケースもあるというだけで、アプリの規模・性質によっては1つしかエクスポートしないという原則も通用するかもしれないとも思っています。というのも、モジュールをカプセル化の手段と見なすと、1つしかexportしないモジュールというのは1つしかメソッドを持たないクラスと似たものであることに気づきます。そして、そういったものは大抵クラスではなくただの関数でよいでしょう。実際JavaScript/TypeScriptでは「クラスは不要である」という風潮が強く、それは言い換えればクラスによるカプセル化自体をあまり必要としないということでもあります。ということは、モジュールによるカプセル化も案外レアケースかもしれません。

問題意識について

順番が前後しましたが、元記事にはnamed exportを使わないことのモチベーションについて次のように書かれています。

named exportを積極的に使うことを許してしまうと、そのファイルの「目的」とは何の関係もない(本来そのファイルに置くべきでない)かもしれない定義をそのファイルからexportすることを積極的に許してしまうことに繋がるからです。

この状態がよろしくないということには筆者も同意するところであり、この前提には同意できる人が多いのではないでしょうか。本来関係ない機能の間に(モジュールスコープを通じた)余計な依存関係を発生させないために、必要なければ別のモジュールに分けることは有用です。

ただ、そのための攻撃対象としてnamed exportを選んだ点については、やや一般に受け入れにくい「ブラックボックス」の定義を経由しなければいけなくなってしまったためベストな方法ではないというのが筆者の意見です。

「関係ないものを同じモジュールに入れるべきではない」という主張であれば結構同意されるのではないでしょうか。元記事では周辺ツールの進化による問題解決にも言及していますから、例えばESLintで意味のない同居を禁止する方法を模索するといった方向性の方が建設的かもしれません。

宣伝

元記事には、もう一つ筆者が賛同しかねるところがあります。それは、次の記述です(強調は筆者)。

すべてをnamed exportにした場合でも適切に設計すれば問題ないかもしれませんが、最悪なケースでは、見通しの観点から言って、プロジェクト全体が単一のモジュールで構成されているのとさほど変わらないような状況に陥ってしまいます。つまり、ファイルという単位に本来与えられていたはずのモジュールという意味が失われていってしまい、どの定義がどこに入っているのかがめちゃくちゃになってしまいます。もはや粒度とか以前の問題です。

つまり、1ファイルから複数exportすることは「どの定義がどこに入っているのかがめちゃくちゃ」という状況につながるというのです。確かにその状況は良くありません。そして、元記事の主旨からして、1ファイル1exportにすればこの問題を回避できるという主張が読み取れます。

筆者の考えでは、良くなりません。1ファイル1exportにすると、どのファイルがプロジェクト内のどこにあるのかめちゃくちゃという状況に進化し、本質的な改善は起こらないと思います。元記事には“一望できるディレクトリ構造を利用してプロジェクトの論理構造を表現しましょう。”とありますが、幻想だと思います。論理構造は表現されません。もちろんそれは適切なディレクトリ構造の“設計”により回避できることですが、元記事の立場では設計による治安維持を想定しません。

前提という語彙を使うのであれば、named exportを適切に管理できない人がディレクトリ構造を適切に管理できるだろうという前提はあまり現実的でないように思え、同意できないところです。

これはJavaScriptのモジュールの性質1によるもので、どこからでも好きなモジュールをインポートできるためです。ディレクトリ構造をどのようにしたとしても、“設計”を超えたレベルでモジュール間の関係を表現することはできません。

ここで朗報です。筆者が開発したeslint-plugin-import-accessを使えば、同じディレクトリにあるモジュールからのみインポートできるexportを書くことができます。1ファイル1exportにするにしても、これを使えば論理的に意味のある形で見通しを良くできるかもしれません。

その他の点について

元記事では「default exportのデメリットへの反論」として他にもいくつかの議論がされています。ここはあまり主張の本質的な部分に関わっていないと思われたのでさらっと感想を述べます。

CommonJSとの相互運用

これはトランスパイラの問題であって、default exportを避ける(すなわち、ファイル単位でのモジュール化という理念を壊す)べき本質的な理由にはなりえません。

それな(同意)

TypeScriptにesModuleInteropが導入される前はCommonJSとの相互運用性がかなり厄介な問題でしたが、今は本質的な問題とはなりません。

また、「default exportだと補完が効かない」と思っている人が多いようですが、それはanonymous default exportだからです。名前は付けてください。

たしかに(同意)

従属物とのexportをどうするか

型定義ではなく、従属的な定数などをexportしたい場合には、シンプルにプロパティを生やしてください。TSはトップレベルにおいてのみこの手の代入を許し、代入された側の型も更新します。

Tree shakingに悪影響を与えるからやめてほしいです。

コメントには次のように補足されていますが、このような設計はモジュール側がモジュールを使う側のことを知らなければいけないのでおかしいです。

また、一つのファイルにまとめるべきであるくらい互いに密接に関わるロジックなら、そのモジュールを使ううえでそれらを余すところなく使うはずなので、tree shakingは不要です。

さらに、パフォーマンスの観点では、プロパティアクセスよりnamed exportの方がminificationが効くので有利ですから、named exportを優先して使うべきです。JavaScriptの性質上(残念なことですが)、この問題をツールの進歩によって解決することは原理的に不可能に非常に近いです。

ただ、「設計レベルの話をしているのに何バイトかも分からないマイクロパフォーマンスチューニングを持ち出されても困る」という反論は成り立つかもしれません。個々のケースについては筆者としてもどちらでも良いのですが、ベストプラクティスとしてminificationが効かない方法を推奨することは抵抗があります。

結論

元記事の主旨については結論にまとまっている通り、「default exportを使い、1ファイル1エクスポートをできるだけ徹底する」ことがモチベーションです。1ファイル1エクスポートについては比較的実践可能なプラクティスであり、この点には同意する人が多いのではないでしょうか。筆者としても、可能な限り別のファイルに分けるべきだと思います。ただし、筆者としてはそれに収まらないシチュエーションがあると考えており、ルールとして強制するべきとまでとは考えていません。

また、1つのnamed exportよりもdefault exportを優先する根拠は弱く、記事のタイトルにもなっている「named exportは有害」という主張はあまり賛同できないと感じられました。特に、「モジュールはブラックボックスだから何がnamed exportされているか外から知るべきではない」という主張は特異な前提であり、賛同しにくいものであると思われました。


  1. より正確に言えば、現在JavaScriptのモジュールを活用するエコシステムの性質です。なぜなら、モジュールからのimportを解決する具体的な方法はJavaScriptの言語仕様に含まれず、ホストに委ねられているからです。