HTML 要素の innerText プロパティで要素が生成されうる2021年12月10日 11時13分

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


HTML 標準では、HTML 文書を JavaScript などのプログラミング言語から扱うためのインターフェイスも定義しています。そのひとつが HTML 要素の innerText プロパティ、要素の内容を文字列として取得・設定するプロパティです。かつて Internet Explorer が独自に実装し、他の Web ブラウザも追従した結果として標準化されたものですね。

innerText プロパティの値の取得

innerText プロパティで取得できる値は、ブラウザにレンダリングされたようなテキスト内容となります。CSS で display: none; が指定された要素の内容は含まれませんし、display: block; が指定された要素の内容の前後には改行文字 (U+000A) が挿入されます。表のセル同士の間にはタブ文字 (U+0009) が挿入されます。

そのため、innerText プロパティの値を取得する際にはブラウザの描画処理、いわゆるリフローが走ることになります。スタイルを気にせず単に要素のテキスト内容を取得したいときは、innerText プロパティではなく textContent プロパティを使ったほうがよいでしょう。textContent プロパティの値の取得時にはリフローが走りません。

innerText プロパティの値の設定

innerText という名前からすると値の設定時には単なるテキストノードが生成されそうな気もしますが、HTML 要素が生成されることもあります。設定する値に改行文字が含まれるときは、その改行文字が br 要素に置換されるのです。

単に要素のテキスト内容を設定したいときは、innerText プロパティではなく textContent プロパティを使ったほうがよいでしょう。textContent プロパティの値の設定時には HTML 要素が生成されません。

参考文献

JavaScript の正規表現で複数文字からなる絵文字を扱えるようにする提案2021年12月04日 23時31分

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


今やあちこちで使われている絵文字ですが、その中には 1 文字に見えるのに複数の文字 (符号位置) から構成されるものがあります。例えば「👨‍👩」という男女が並んだ絵文字は、U+1F468 MAN、U+200D ZERO WIDTH JOINER、U+1F469 WOMAN の 3 つの符号位置からなります。

JavaScript の正規表現でこのような絵文字にマッチさせようとすると正規表現パターンが長大になってしまいます。また、文字クラス [...] 内にこのような絵文字を記述すると、「U+1F468、U+200D、U+1F469 という符号位置の並び」ではなく「U+1F468、U+200D、U+1F469 のいずれかの符号位置」にマッチしてしまいます (/u フラグが有効な場合)。

これを解決するための提案が ECMAScript proposal: support properties of strings (a.k.a. “sequence properties”) in Unicode property escapes です。Unicode において符号位置の並びに対して定義されているプロパティを、正規表現パターンで扱えるようにしようというもので、2021 年 12 月現在は stage 2 となっています。

これが実現すれば、\p{RGI_Emoji} という正規表現パターンが「👨‍👩」という符号位置の並びにマッチするようになります。さらに、このパターンは文字クラス [...] の内部で使うこともできます。ひとつの文字クラスにマッチするのはひとつの符号位置 (/u フラグが無効な場合はひとつの符号単位) というこれまでの常識が覆されるのです。

個人的には、将来的に JavaScript 組み込みの機能で Unicode の書記素クラスタを扱えるようにならないかと期待しています。

参考文献

Node.js ではイベントから非同期イテレータを生成できる2021年12月02日 23時37分

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


Node.js の Events モジュールでは、events.on メソッドを提供しています (EventEmitter オブジェクトの on メソッドとは別物です)。これは、イベントが列挙されるような非同期イテレータを生成するものです。第 1 引数にイベントを発生させる EventEmitter オブジェクトまたは EventTarget オブジェクトを、第 2 引数に列挙したいイベントの名前を指定します。

以下の例では、実行すると 1 秒後と 3 秒後にそれぞれ「foo イベントが発生しました」と出力されます。

// events-on.mjs

import { on } from 'events';

const target = new EventTarget();
setTimeout(() => {
  target.dispatchEvent(new Event('foo'));
}, 1000);
setTimeout(() => {
  target.dispatchEvent(new Event('foo'));
}, 3000);

(async () => {
  for await (const [event] of on(target, 'foo')) {
      console.log(`${event.type} イベントが発生しました。`)
  }
})();

RxJS の Observable.fromEvent メソッドと機能が似ていますね。

ブラウザ上でも使えると面白い書き方ができるのかもしれませんが、Events モジュールのブラウザ用実装では events.on メソッドに対応していません。issue は立っており、作業ブランチも存在するものの、半年以上動きがなさそうです。

TypeScript で string 型の値に自動補完を効かせる2021年09月11日 12時12分

結論

type X = 'foo' | 'bar' | (string & {});

のように、文字列リテラル型の共用体型に | (string & {}) を付け足した型 X を定義します。X 型は任意の文字列を受け付けますが、IDE (Visual Studio Code など) で X 型の値を入力するときには 'foo''bar' が自動補完の候補として提示されます。

解説

単純に type X = 'foo' | 'bar' | string; と書いてしまうと、共用体型の各要素がまとめられて、X は単なる string 型になってしまいます。{} 型は nullundefined を除く任意の値を受け付けるので、string & {} 型は実質的に string 型と同一なのですが、TypeScript 4.4 の時点では同一扱いされず、共用体型の各要素がまとめられることもありません。

この手法を知った経緯

React の型定義の変更履歴を見たところ、input 要素の type 属性の型定義を追加するという変更が入っていました。そこには見慣れない表記が交じっており、コメントには「| (string & {}) ハック」と書かれています。

input 要素の type 属性 (HTMLInputElement インターフェイスの type プロパティ) には、形式的には任意の文字列を指定できますが、実際に有効なのは textcheckboxradio など特定の値のみです。type プロパティの値を指定するときに、自動補完を使ってミスなく高速に入力したいというのは自然な考えでしょう。

しかし、type プロパティの型を string 型から文字列リテラル型の共用体型に変えてしまうと、これまで指定できていた値が指定できなくなったり、将来の input 要素の拡張に即座に対応できなくなったりと、互換性が失われてしまいます。型の互換性を維持しつつ IDE の入力支援を引き出すために、| (string & {}) ハックが生み出されたのだと思います。

TypeScript でモジュール内でのみ参照可能なグローバル変数を宣言する2021年03月21日 15時34分

Web ブラウザで使われる JavaScript ライブラリの中には、グローバル変数 (window オブジェクトのプロパティ) をはやすものがあります。Google Tag Manager の window.dataLayer や、Canva ボタンの window.Canva などです。

そうしたグローバル変数をあちこちのモジュールから直接操作していると保守性が下がってしまうので、ラッパーとなるモジュールを用意したいところです。グローバル変数を直接操作するのはラッパーモジュール内のみにとどめ、他のモジュールはラッパーモジュールを介して外部ライブラリにアクセスするという仕組みです。

このとき、ラッパーモジュール内でグローバル変数にアクセスしつつ、他のモジュールではそのグローバル変数にアクセスできないようにするには、どうしたらよいでしょうか。

TypeScript でのグローバル変数の宣言は以下のように declare global を使うのが基本ですが、これだと他のモジュールでも window.dataLayer にアクセスできてしまいます。

declare global {
  interface Window {
    dataLayer: object[];
  }
}

ラッパーモジュール内で以下のように window 変数を宣言しなおすことで、グローバル変数を参照できる範囲がラッパーモジュール内に限定されます。(「グローバル変数」といいつつ、window オブジェクトのプロパティとしてアクセスすることになりますが。)

declare const window: Window['window'] & {
  dataLayer: object[];
};

// このモジュール内では `window.dataLayer.push(...)` と書ける。

Window['window'] はもともとの window オブジェクトの型を指します。それとの交差型 (intersection types) を取ることで、window オブジェクトの既存のプロパティ (window.setTimeout など) にも、新たに宣言したプロパティにもアクセスできます。