TypeScript で innerText の ts2339 エラーを対処する

TypeScript で HTML DOM 操作していた際に innerText プロパティを参照したところ ts2339 エラーが発生したので、原因と対処方法について紹介します。

やりたいこと

HTML 内の <span class="data"> 要素を全部取ってきて、いくつかの要素内のテキストを取得したかったので、以下のようなコードを TypeScript で書きました。

const spans = document.querySelectorAll('span.data');
const data1 = spans[0].innerText;
const data2 = spans[5].innerText;

ところが、Property 'innerText' does not exist on type 'Element'. (ts2339) と TypeScript のエラーがでます。Element 型に innerText というプロパティはないといわれています。

このエラーの原因の調査と、対処方法について紹介します。

環境

ブラウザ
Microsoft Edge 121.0.2277.112
typescript
5.2.2

HTML DOM API の仕様

TypeScript の前に、まず HTML DOM API 自体の仕様を確認しようと思い、MDN で innerText を調べます。

innerTextHTMLElement のプロパティで、ノードとその子孫の「レンダリングされている」テキスト内容を示します。

HTMLElement: innerText プロパティ - Web API | MDN

なるほど、innerTextHTMLElement のプロパティなので、HTMLElement の継承元である Element は確かに innerText プロパティはないわけですね。

ここでふと、そもそも querySelectorAll() が返す型ってなんだっけと思い、こちらも調べます。

生きていない NodeList で、指定されたセレクターの 1 つ以上に一致する子孫ノード 1 つに対して 1 つずつの Element を含みます。

Element: querySelectorAll() メソッド - Web API | MDN

予想通りですが、ElementNodeList でした。TypeScript のエラーが言っていることは正しいわけですね…。

なお、蛇足ですが、innerTextHTMLElement のプロパティでしたが、innerHTMLElement のプロパティなんですね。知りませんでした…。

Element オブジェクトの innerHTML プロパティは、要素内の HTML または XML のマークアップを取得したり設定したりします。

Element: innerHTML プロパティ - Web API | MDN

TypeScript の仕様

原因はほぼわかったようなものですが、念のため TypeScript の仕様も確認してみました。

querySelectorAll<K extends keyof HTMLElementTagNameMap>(selectors: K): NodeListOf<HTMLElementTagNameMap[K]>;
querySelectorAll<K extends keyof SVGElementTagNameMap>(selectors: K): NodeListOf<SVGElementTagNameMap[K]>;
querySelectorAll<E extends Element = Element>(selectors: string): NodeListOf<E>;

TypeScript: Documentation - DOM Manipulation

驚きだったのが、私は 3 つ目の仕様のみを期待していたのですが、getElementsByTagName のような使い方で、引数に HTML や SVG のタグ名のみを渡す場合はそのタグに応じた型の NodeListOf で返してくれるようです。

実際に試してみましたが、以下の元のコードでは spansNodeListOf<Element> 型でしたが、

const spans = document.querySelectorAll('span.data');
const data1 = spans[0].innerText;
const data2 = spans[5].innerText;

以下のように querySelectorAll() の引数をタグ名にしてみると、spansNodeListOf<HTMLSpanElement> 型に型推論され、HTMLSpanElementHTMLElement を継承しているので、innerText プロパティを参照しても冒頭の ts2339 エラーはでなくなりました。

const spans = document.querySelectorAll('span');
const data1 = spans[0].innerText;
const data2 = spans[5].innerText;

型アサーションで対応

さて、TypeScript で querySelectorAll() の引数でタグ名を指定した場合は、そのタグに応じた型に型推論がなされることはわかりましたが、今回はタグ名ではなく、クラス名を含めたセレクタ文字列を渡したかったので、型アサーションで ts2339 エラーを回避したいと思います。

const spans = <NodeListOf<HTMLSpanElement>>document.querySelectorAll('span.data');
const data1 = spans[0].innerText;
const data2 = spans[5].innerText;

または

const spans = document.querySelectorAll('span.data') as NodeListOf<HTMLSpanElement>;
const data1 = spans[0].innerText;
const data2 = spans[5].innerText;

と型アサーションをつけることで ts2339 エラーはでなくなります。

なお、innerText プロパティは HTMLElement のプロパティなので、厳密に NodeListOf<HTMLSpanElement> としなくても NodeListOf<HTMLElement> でも問題ありません。

また今回セレクタの全要素が欲しかったので querySelectorAll() を使用していますが querySelector() の場合は、

const span = <HTMLSpanElement>document.querySelector('span.data');

または、

const span = document.querySelector('span.data') as HTMLSpanElement;

のように型アサーションします。

あとがき

TypeScript で 直に HTML DOM 操作することってそれほど多くないと思うのですが、たまたま実装する機会があって、結果勉強になりました。