なぜ ES2022 で文字列リテラルを使った import/export ができるようになるのか

Published on

11 月 11 日に、以前から一部で注目されていたある Pull Requesttc39/ecma262 にマージされました。

この Pull Request がマージされたことで、識別子ではなく文字列リテラルを使った import/export が可能になりました。

この仕様変更はプロポーザルという形で扱われてはいませんが、構文上の影響があるので、JavaScript ユーザーとして知っておくに越したことはないものになります。

概要

まず具体例を示します。

今回の変更によって、次のように import/export する際の名前として文字列リテラルを使えるようになります。

const foo = "foo";
export { foo as "😃 hey hey" };
import { "😃 hey hey" as foo } from "./module.js";
console.log(foo); // foo

基本的にはこれだけです。

詳解

ここからは仕様上の用語を使って解説をします。

この変更が入る前の ECMAScript では ImportSpecifieras を使う場合 as の左側は IdentifierName でなければいけませんでした。 また ExportSpecifier は、単一の IdentifierName もしくは、as を使う場合は as の左側と右側は両方とも IdentifierName でなければいけませんでした。

今回の変更によって、新たに ModuleExportName という構文が追加されました。ModuleExportName は、IdentifierName もしくは StringLiteral の形をとります。 たとえば、識別子 foo や 文字列リテラル "😃 hey hey"ModuleExportName です。

そして、ImportSpecifieras を使う場合 as の左側は ModuleExportName の形をとるようになりました。したがって、次の例の import 文はすべて構文として妥当です。

import { foo } from "mod";
import { foo as bar } from "mod";
import { "😃 hey hey" as baz } from "mod";

ExportSpecifier は、単一の ModuleExportName もしくは、 as を使う場合 as の左側と右側は両方とも ModuleExportName の形をとるようになりました。したがって、次の例の export 文はすべて構文として妥当です。

export { foo } from "mod";
export { "😃 hey hey" } from "mod";
export { foo as foo } from "mod";
export { foo as "😃 hey hey" } from "mod";
export { "😃 hey hey" as foo } from "mod";
export { "😃 hey hey" as "😃 hey hey" } from "mod";

ただし ExportSpecifierModuleExportNameStringLiteral にできるのは、その ExportSpecifier を含む ExportDeclarationFromClause が存在する場合のみです。

たとえば、次のコードは ExportDeclarationFromClause が存在しないので ExportSpecifierStringLiteral を使うことはできません。

// できない
export { "😃 hey hey" };

一方で、次のコードは FromClause が存在するので、ExportSpecifierStringLiteral を使うことができます。

// できる
export { "😃 hey hey" } from "mod";

文字列の制約

StringLiteral は通常の JavaScript の文字列リテラルです。たとえば "foo" とか "bar" みたいな形をしたものです。

ModuleExportNameStringLiteral を含むので、全ての文字列リテラルを ModuleExportName として使えるようにみえますが、実際には少々異なります。ModuleExportName として使える StringLiteral には制限があります。

ModuleExportName として使える StringLiteral は、Well-Formed Code Unit Sequence でなければいけません。 このことは、Module Semantics の Eary Errros 内の https://tc39.es/ecma262/#_ref_6583 に記載されています。

Well-Formed Code Unit Sequence とは

JavaScript の文字列は UTF-16 でエンコードされます。そのため、実際には JavaScript の文字列というのは 16 ビットの整数で表現される Unicode のコードユニットの並びでしかありません。

UTF-16 では基本的に 1 文字につき 16 ビットで表現されます。しかし、Unicode の BMP(基本多言語面)に収まらない文字は 16 ビットのコードユニットを二つ並べたペアで表現します。

たとえば、ひらがなの は BMP に含まれており、一つのコードユニット(0x3042)で表されます。

console.log("\u3042"); // あ

一方で、𠮟( ではないことに注意) は BMP に含まれないので、二つのコードユニット(0xD8420xDF9F)で表されます。このようなコードユニットのペアを、サロゲートペアといいます。

console.log("\uD842\uDF9F"); // 𠮟

前述のとおり、JavaScript の文字列は 16 ビットの整数で表現されるコードユニットの並びでしかありません。したがって、𠮟 を構成する二つのコードユニットである 0xD8420xDF9F のうち一つだけを含む文字列も作ることができます。

const str = "\uD842";

しかし、0xD842 単体に対応する文字は Unicode には存在しません。

このような、対になっていないサロゲートペアを含むような文字列は Well-Formed Code Unit Sequence ではありません。

逆に、対になっていないサロゲートペアを許容しないような文字列を Well-Formed Code Unit Sequence といいます。つまり、大雑把にいえば「ちゃんと文字になっているコードユニットで構成された文字列」ということです。

ちなみに、このような Well-Formed な文字列は WebIDL では USVString と呼ばれています。

新しい Abstract Operation IsStringWellFormedUnicode

この仕様の変更に伴って、IsStringWellFormedUnicode という新しい Abstract Operation が追加されました。

この Abstract Operation は、引数の文字列が Well-Formed Code Unit Sequence かどうかを判定します。

前述した ModuleExportName のための Early Errors では、この IsStringWellFormedUnicode Abstract Operation を使って StringLiteral が Well-Formed Code Unit Sequence かどうかの判定を行います。そしてもし Well-Formed Code Unit Sequence でなければ Syntax Error になります。

仕様変更のモチベーション

実はこの仕様の変更は、今の Web の仕様ではほとんど役に立つことはありません。

この変更が行われたモチベーションは、将来的に WebAssembly の Module との相互運用性を向上させるためです。

この背景を理解するために、おさえておくべき前提が二つあります。

1つ目は、WebAssembly の Module では関数を export するときに文字列で名前をつけるということです。 たとえば次の例では $add という関数を "add" という名前で export しています。

(module
  (func $add (param $lhs i32) (param $rhs i32) (result i32)
    local.get $lhs
    local.get $rhs
    i32.add)
  (export "add" (func $add))
)

2つ目は、WebAssembly の Module を JavaScript から import できるようにしたい、という動きがあるということです。WebAsembly/esm-integration などで、その動きを見ることができます。

簡単にいえば、次のようにして簡単に WebAssembly の Module を JavaScript から扱えるようにしたいということです。

import { add } from "foo.wasm";
console.log(add(1, 2)); // 3

現在の WebAssembly および ECMAScript の仕様では、このような形で JavaScript 側から WebAssembly の Module を読み込むことはできません。

これらを前提として上で、次の例について考えます。

この例は前述したものとほとんど変わりませんが、export の後ろが "add" ではなく "+" になっています。export の後ろには文字列を置くことができるので、これは妥当な Module です。

(module
  (func $add (param $lhs i32) (param $rhs i32) (result i32)
    local.get $lhs
    local.get $rhs
    i32.add)
  (export "+" (func $add))
)

将来、WebAssembly の Module を JavaScript から import できるようになったときに、このモジュールから + 関数を named import したいとします。 しかし、+IdentifierName ではないので、今までの ECMAScript の仕様では named import できませんでした。

// できない
import { + } from "foo.wasm";
// できない
import { + as add } from "foo.wasm";

今回の変更によって ImportSpecifieras の左側に StringLiteral を置けるようになったことで、次のように書るようになりました。

// ES2022 でできる
import { "+" as add } from "foo.wasm";

console.log(add(1, 2)); // 3

このような書き方は ES2022 では構文上は妥当ですが、実際にはまだ WebAssembly の import はできません。

また、ModuleExportNameStringLiteral が Well-Formed Code Unit Sequence でなければならないという制約が存在するのも、WebAssembly との相互運用のためです。 WebAssembly のテキストフォーマットで export の後に続く文字列は Well-Formed Code Unit Sequence でなければいけないので、それと統一させたのでしょう。

参考リンク

ツイート

記事に関する報告などはこちらから
;