JavaScriptの識別子に中黒が使えるようになった2024年06月07日 10時21分

JavaScriptの識別子(変数名、関数名、プロパティ名など)の2文字目以降に中黒「・」(U+30FB KATAKANA MIDDLE DOT)が使えるようになりました。以下のコードはChrome 124では構文エラーになりますが、Chrome 125では問題なく実行できます。

const シン・ゴジラ = 2016;

JavaScriptの識別子

中黒が使えるようになったのは、JavaScript(ECMAScript)の仕様が変わったからではありません。変わったのはUnicodeの仕様のほうです。Unicode 15.1.0(2023年9月)においてOther_ID_Continueプロパティ(を持つ文字の集まり)に中黒が追加されました。

そもそもJavaScriptの識別子に使える文字は、Unicodeを参照して定義されています。ECMAScript 2023(2023年6月)では以下のようになっています。

識別子の1文字目に使える文字
  • UnicodeのID_Startプロパティを持つ文字
  • $(U+0024 DOLLAR SIGN)
  • _(U+005F LOW LINE)
識別子の2文字目以降に使える文字
  • UnicodeのID_Continueプロパティを持つ文字
  • $(U+0024 DOLLAR SIGN)
  • ゼロ幅非接合子(U+200C ZERO WIDTH NON-JOINER)
  • ゼロ幅接合子(U+200D ZERO WIDTH JOINER)

(いわゆるアンダースコア「_」はID_Continueプロパティを持つため、2文字目以降にも使えます。)

Unicodeの仕様も毎年のように改定されますが、ECMAScript仕様ではUnicodeの「最新バージョン」が参照されています。

Unicodeの識別子

UnicodeのID_StartID_Continueプロパティは、各種の「識別子」に使える文字として推奨されるものを表しており、UAX #31 Unicode Identifiers and Syntaxで以下の文字を含むものとして定義されています。

ID_Startプロパティ
  • 一般カテゴリがLetterである文字
  • 一般カテゴリがLetter_Numberである文字
  • Other_ID_Startプロパティを持つ文字
  • ただしPattern_SyntaxプロパティまたはPattern_White_Spaceプロパティを持つ文字を除く(具体的には(U+2E2F VERTICAL TILDE)が除かれる)
ID_Continueプロパティ
  • ID_Startプロパティを持つ文字
  • 一般カテゴリがNonspacing_Markである文字
  • 一般カテゴリがSpacing_Markである文字
  • 一般カテゴリがDecimal_Numberである文字
  • 一般カテゴリがConnector_Punctuationである文字
  • Other_ID_Continueプロパティを持つ文字
  • ただしPattern_SyntaxプロパティまたはPattern_White_Spaceプロパティを持つ文字を除く

ここでOther_ID_StartプロパティとOther_ID_Continueプロパティというのは、後方互換性のためにそれぞれID_StartプロパティとID_Continueプロパティに含めるべき文字を表しています。Other_ID_Continueプロパティに中黒(U+30FB)が追加されたことで、巡り巡ってJavaScriptの識別子に中黒が使えるようになったのです。

なお、中黒だけでなくゼロ幅非接合子(U+200C)とゼロ幅接合子(U+200D)も追加されたため、ECMAScript 2024以降では識別子の2文字目以降に使える文字の定義が「UnicodeのID_Continueプロパティを持つ文字または$(U+0024 DOLLAR SIGN)」と簡潔になる予定です。

過去にも識別子に中黒が使えた

ID_Continueプロパティの後方互換性のために中黒が追加されたということは、さらに以前は中黒がID_Continueプロパティに含まれていたのでしょうか?

まさにその通りで、Unicode 4.0.1(2004年5月)以前は中黒の一般カテゴリがConnector_Punctuationになっており、結果としてID_Continueプロパティに含まれていました。Unicode 4.1.0(2005年3月)で中黒の一般カテゴリがOther_Punctuationに変更され、ID_Continueプロパティに含まれなくなっていたのです。

ECMAScript 5.1ではUnicode 3.0以上への適合が求められていたため、ECMAScript 3(1999年12月)~5.1(2011年6月)の間は中黒を識別子に使えた可能性があります。(ECMAScript 2(1998年8月)以前はASCIIの範囲内の文字のみ識別子に使用可能、ECMAScript 2015(ES6、2015年6月)以降はUnicode 5.1.0以上への適合が求められる。)

他のプログラミング言語の識別子

識別子の定義にUAX #31を参照しているのはJavaScriptだけではありません。CC++PerlPythonRustなど多くのプログラミング言語がUAX #31を参照しています。これらの言語でもそのうち識別子に中黒が使えるようになるでしょう(\p{Word}との共通部分を採用しているPerlを除く)。

なお、JavaScript以外ではID_StartID_Continueプロパティではなく、XID_StartXID_Continueプロパティを参照していることが多いです。これらのプロパティは、ある識別子にUnicode正規化を適用しても識別子として有効であり続けることを保証するため、それぞれID_StartID_Continueプロパティからいくつかの文字を除外しています。

参考文献

TypeScriptでイベントをPromise化する関数の型を定義したい2022年12月03日 10時41分

この記事はTypeScript Advent Calendar 2022の3日目の分です。


addEventListenerでリッスンしているイベントをPromise化する」という記事で、イベントをPromiseで受け取る関数が紹介されています。Node.jsのeventsモジュールのevents.onceメソッドと同じ機能を実現するものですね。Webブラウザ組み込みのDOMでも同じ機能を提供しようという提案もなされています。

最初の記事では余談として今回紹介したeventPromisifyはTypeScriptで書こうとすると型の定義が難しいなと思いましたと書かれています。例えばeventPromisify(document, 'click')と呼び出したら返り値の型がPromise<MouseEvent>になってほしいのですが、そのような型定義を記述できるでしょうか? (以下、TypeScript 4.9を想定しています。)

イベントが発生する対象(target)とイベント名(type)を決め打ちできるのなら、

type EventForDocumentClick =
  typeof document.addEventListener<'click'> extends
    (type: 'click', listener: (event: infer E) => void) => void
  ? E : never;
// = MouseEvent

のように、document'click'からMouseEventを導出できます。しかし、変数documentではなくDocument型だけが与えられているとき、Document['addEventListener']<'click'>のように型引数を指定することはできません。

またDocument型におけるaddEventListenerメソッドの定義は、イベント名がDocument固有(keyof DocumentEventMap型)のものとイベント名が文字列全般(string型)のものがオーバーロードされています。型引数が絡んでいなければ、「オーバーロードされた関数型から引数の型や返り値の型を取り出す方法」に書かれているように型を取り出せます。しかし、型引数を持つメソッドがオーバーロードされているときに、意図した型引数が指定された場合の引数や返り値の型を取り出せるのかどうか、私にはわかりませんでした。

結局私にできたのは以下の状態までです(TypeScript Playgroundで確認)。

type EventTypeFor<Target extends EventTarget> = Target['addEventListener'] extends {
  (type: infer T, listener: (e: Event) => void): void;
  (type: string, listener: EventListenerOrEventListenerObject): void;
} ? T : never;

type EventFor<Target extends EventTarget> = Target['addEventListener'] extends {
  (type: string, listener: (e: infer E) => void): void;
  (type: string, listener: EventListenerOrEventListenerObject): void;
} ? E : never;

const eventPromisify = <T extends EventTarget>(
  target: T,
  type: EventTypeFor<T>
): Promise<EventFor<T>> => {
  throw new Error('Not implemented');
}

const p = eventPromisify(document, 'click');
// p: Promise<Event | MouseEvent | UIEvent | ClipboardEvent | AnimationEvent | InputEvent | FocusEvent | ... 11 more ... | WheelEvent>

第2引数をDocument固有のイベント名に限定することはできています。しかしながら、返り値のPromiseの値の型をMouseEventに限定することはできず、Document固有のイベントすべての共用体型となってしまいます。

HTMLのa要素にはhref属性を指定しなくてもよい2022年10月20日 23時30分

HTMLのa要素はハイパーリンクを表す要素であり、リンク先のURLをhref属性に指定します。しかし、a要素の役割はそれだけではありません。HTML標準によれば、a要素は「リンクとなりうる箇所のプレースホルダー」として使うこともできます。この場合はhref属性を指定しません。

リンクとなりうる箇所の例として、ナビゲーションやタブUI、パンくずリストなどでの「現在の項目」があります。

<nav>
  <ul>
    <li><a href="/">ホーム</a></li>
    <li><a>最新記事</a></li>
    <li><a href="/archives">アーカイブ</a></li>
    <li><a href="/settings">設定</a></li>
  </ul>
</nav>

ReactなどJSXでa要素を生成する場合、href属性を指定しないためにはhrefプロパティにundefinedを指定します。

import React from "react";

type Item = {
  label: string;
  url: string;
  isCurrent: boolean;
};

type Props = {
  items: readonly Item[];
};

const Navigation: React.FC<Props> = ({ items }) => (
  <nav>
    <ul>
      {items.map((item) => (
        <li>
          <a href={item.isCurrent ? undefined : item.url}>
            {item.label}
          </a>
        </li>
      ))}
    </ul>
  </nav>
);

リンクのプレースホルダーとしてのa要素は、うまく使えばテンプレートやCSSの記述を簡潔にできます。覚えておいて損はないでしょう。

なお、a要素にhref属性を指定しないと聞いてname属性を指定するのかと思った人もいるでしょうが、現在のHTML標準ではa要素のname属性は廃止済みであり指定すべきでないとされています。

TypeScript の可変長タプル型における共用体の分配2022年06月09日 01時29分

TypeScript の可変長タプル型 (variadic tuple types) とは、配列型やタプル型を展開して別のタプル型の一部として使える機能のことです (「TypeScript 4.0で導入されるVariadic Tuple Typesをさっそく使いこなす - Qiita」に詳しいです)。記法としては、展開する型の直前に三連続のドット ... を記述します。例えば、

type Sandwich<Fillings extends unknown[]> = ['bread', ...Fillings, 'bread'];

のように具材 (filling) をパン (bread) で挟む Sandwich 型があったとき、Sandwich<['ham']> 型は ['bread', 'ham', 'bread'] 型に展開されます。

type HamSandwich = Sandwich<['ham']>;
// → ['bread', 'ham', 'bread']

type BLTSandwich = Sandwich<['bacon', 'lettuce', 'tomato']>;
// → ['bread', 'bacon', 'lettuce', 'tomato', 'bread']

type RichHamSandwich = Sandwich<'ham'[]>;
// → ['bread', ...'ham'[], 'bread']

type TwoSlicesOfBread = Sandwich<[]>;
// → ['bread', 'bread']

ここで型引数 Fillings に共用体型 (縦線 | で区切った複数の型のうちのいずれかを表す型) を渡すとどうなるでしょうか。具材がハムか卵なら、できあがるのはハムサンドか卵サンドになります。つまり、Sandwich<['ham'] | ['egg']> 型は Sandwich<['ham']> | Sandwich<['egg']> 型と同等に扱われます。

type HamOrEggSandwich = Sandwich<['ham'] | ['egg']>;
// → ['bread', 'ham', 'bread'] | ['bread', 'egg', 'bread']

このように、T<A | B | C | (略)> 型が T<A> | T<B> | T<C> | (略) 型として扱われる挙動を「共用体の分配 (union distribution)」と呼びます。 共用体の分配は条件型 P extends Q ? R : S でも発生しますが (「TypeScriptの型初級 - Qiita」に詳しいです)、可変長タプル型でも発生するのです。

条件型における共用体の分配では、never 型を分配しようとすると展開結果も never 型になります。これは可変長タプル型においても同じです。

type ImpossibleSandwich = Sandwich<never>
// → never

なお、可変長タプル型において any 型は any[] 型に展開されます。unknown 型を展開することはできません。

type AnySandwich = Sandwich<any>;
// → ['bread', ...any[], 'bread']

type UnknownSandwich = Sandwich<unknown>;
// → Error: Type 'unknown' does not satisfy the constraint 'unknown[]'.

可変長タプル型で共用体の分配が発生するというのは、可変長タプル型を導入した pull request の説明に書かれています。私はこの挙動を、type-challenges の回答のひとつをきっかけに知りました (その回答は誤答なのですが)。

  • When the type argument for T is a union type, the union is spread over the tuple type. For example, [A, ...T, B] instantiated with X | Y | Z as the type argument for T yields a union of instantiations of [A, ...T, B] with X, Y and Z as the type argument for T respectively.

Variadic tuple types by ahejlsberg · Pull Request #39094 · microsoft/TypeScript

HTML のフォームコントロール要素と label 要素の紐づけ2021年12月24日 21時11分

この記事は HTML アドベントカレンダーの 24 日目の分、兼 JavaScript アドベントカレンダーの 24 日目の分です。


HTML のフォームコントロール要素 (inputtextareaselectbutton 要素など) には、label 要素を使ってラベルを指定できます。ここでいうラベルとは、そのフォームコントロールに何を入力するか・そのフォームコントロールで何ができるのかの簡単な説明であり、人間が読んで理解できるようなフォームコントロールの名前です。

ある label 要素の子孫にフォームコントロール要素が存在すれば、その label 要素の内容が、そのフォームコントロール要素のラベルとなります。そうでない場合、label 要素の for 属性にフォームコントロール要素の ID (id 属性の値) を指定する必要があり、その label 要素の内容が、その ID を持つフォームコントロール要素のラベルとなります。

このフォームコントロール要素と label 要素との紐づきは JavaScript を使って参照できます。フォームコントロール要素オブジェクトの labels プロパティはそのフォームコントロール要素と紐づく label 要素の一覧 (NodeList オブジェクト) を返し、label 要素オブジェクト (HTMLLabelElement オブジェクト) の control プロパティはその label 要素に紐づくフォームコントロール要素を返します。

labels プロパティの名前が複数形なのは、ひとつのフォームコントロール要素に対して複数の label 要素を紐づけられるからですね。

<label id="query-label-1" for="query-field">キーワード</label>
<label id="query-label-2" for="query-field">URL</label>
<input id="query-field" type="search" name="q">
const label1 = document.getElementById('query-label-1');
const label2 = document.getElementById('query-label-2');
const field = document.getElementById('query-field');

console.assert(label1.control === field, 'control プロパティでフォームコントロールを参照できる (1)');
console.assert(label2.control === field, 'control プロパティでフォームコントロールを参照できる (2)');
console.assert(field.labels[0] === label1, 'labels プロパティで label 要素を参照できる (1)');
console.assert(field.labels[1] === label2, 'labels プロパティで label 要素を参照できる (2)');

あるフォームコントロールにおいて、labels プロパティの返す NodeList オブジェクトは常に同一です。紐づく label 要素に変更があれば、その NodeList オブジェクトの内容が動的に変化します。

const oldLabels = field.labels;
label1.remove();
const newLabels = field.labels;

console.assert(oldLabels === newLabels, 'labels プロパティの値は何度参照しても同一のオブジェクトである');
console.assert(oldLabels.length === 1, 'labels プロパティの値は動的に変化する');

ただし、実際のところひとつのフォームコントロール要素に複数の label 要素を紐づけるような場面はほとんどないと思います。

button 要素にも label 要素を紐づけられます。しかしながら、button 要素の場合は自身の内容がラベルとして扱われるので (<button type="submit">検索する</button> なら「検索する」がそのボタンのラベルになります)、実際のところ button 要素に label 要素を紐づけるような場面はほとんどないと思います。

ラベルは aria-label 属性aria-labelledby 属性を使って指定することもできます。

<form action="/search">
  <p>
    <input type="search" name="q" aria-label="キーワード">
    <button type="submit" aria-label="検索する">🔍</button>
  </p>
</form>