前置き
今年の4月からTypeScriptを使用する現場で働いています。
実務でTypeScriptを使用したときに、最初にぶつかったのがタイトルのエラーでした。
本記事ではこのエラーはどういう意味なのか、理解のために必要なことをまとめます。
要素はそれぞれ独自のDOMインターフェースを持っている
プロパティ 'hoge' は型 'Fuga' に存在しません
例:プロパティ 'value' は型 'Element' に存在しません。
const inputElement = document.querySelector('#hoge');
if (!inputElement) return;
const value = inputElement.value; // エラー:プロパティ 'value' は型 'Element' に存在しません。
このようなエラーに何度も遭遇しました。
document.querySelector() で要素を取得するとElement型が返却されますが、valueプロパティはElement型には存在しないため、使用できませんというエラーです。
要素はそれぞれ独自のインターフェースを持っており、上記のエラーを解消するためにはvalueプロパティを持つ要素だということをTypeScriptに伝える必要があります。
例のコードはinput要素を取得しています。
input要素のDOMインターフェースの型を、以下のようにジェネリクスを使用して戻り値の型をより具体的に指定するとエラーが解消されました!
const inputElement = document.querySelector<HTMLInputElement>('#hoge'); // 戻り値の型がHTMLInputElementであると指定する
if (!inputElement) return;
const value =inputElement.value; // エラーが解消された!
毎度なるほどと思うのですがなかなか覚えられずにいたのでしっかり調べてみました。
DOMインターフェースとは
DOMインターフェースとはウェブページの中身を操作するためのツールのことです。
これを使うことで、ウェブページのテキストを変えたり、画像を動かしたり、ボタンが押されたときに何かを起こしたりすることができます。
DOMインターフェースの最上位にはEventTargetインターフェースがあり、次にNodeというインターフェースがあります。
EventTargetは addEventListenerなどのイベント関連の処理なので、本記事ではwindow.document内部で意識するべきNodeについて説明します。
Node はドキュメントツリーの各要素を表現し、子ノードのリストを取得したり、親Nodeを取得したり、Nodeを追加・削除するためのメソッドが定義されています。
スーパータイプとサブタイプ
「スーパータイプ」と「サブタイプ」は、主にオブジェクト指向プログラミングにおける型の階層構造を表すための用語です。
スーパータイプ(または親型、基底型)
一つ以上の「サブタイプ」(または子型、派生型)を持つことができる型を指します。
スーパータイプは、そのサブタイプが共通に持つべき特性や振る舞いを定義します。
サブタイプ(または子型、派生型)
スーパータイプから特性や振る舞いを継承し、それに追加や特化を行った新しい型を指します。
サブタイプはスーパータイプの特性や振る舞いを「再利用」することで、同じコードを何度も書くことなく新しい機能を提供できます。
図示するとこのようになります
Node (スーパータイプ)
  |
  +---- Element (サブタイプ | スーパータイプ)
  |      |
  |      +---- HTMLElement (サブタイプ | スーパータイプ)
  |      |      |
  |      |      +---- HTMLDivElement (サブタイプ)
  |      |      |
  |      |      +---- HTMLAnchorElement (サブタイプ)
  |      |      |
  |      |      +---- HTMLInputElement (サブタイプ)
  |      |
  |      +---- SVGElement (サブタイプ)
  |
  +---- CharacterData (サブタイプ | スーパータイプ)
         |
         +---- Text (サブタイプ)
         |
         +---- Comment (サブタイプ)
NodeはElementやCharacterDataなどのスーパータイプとなります。
さらに、ElementはHTMLElementとSVGElementのスーパータイプで、これらもまたそれぞれのサブタイプ(HTMLDivElement、HTMLAnchorElement、HTMLInputElementなど)を持っています。
これらのサブタイプはスーパータイプから特性や振る舞いを継承し、その上で自身の特性や振る舞いを追加しています。
上部で取り扱った例をもう一度振り返ってみましょう。
const inputElement = document.querySelector('#hoge');
ここで取得した要素の型は Element なので、Nodeと、Elementのインターフェースが使用できます。
ですが、まだ取得した要素が HTMLInputElement だと指定していないので、HTMLInputElementインターフェースは使用できません。
// NodeのプロパティであるtextContentが使用できる
inputElement.textContent = 'hoge';
// ElementのプロパティであるclassNameが使用できる
const className = inputElement.className;
// HTMLInputElementのプロパティであるvalueは使用できない
const value = inputElement.value; // エラー:プロパティ 'value' は型 'Element' に存在しません。
このインターフェースのおかげで、プログラムの中でHTML要素を扱う際に、その要素が持つべき特性や振る舞いについてコンパイラがチェックしてくれます。
これは意図しない操作を未然に防ぐために有用です。
asキーワードを用いた型アサーションでも同じ?
TypeScriptに型を教えるには、asキーワードを用いて型アサーションする方法もあります。
const inputElement = document.querySelector('#hoge') as HTMLInputElement;
const value = inputElement.value; // エラーは起きない
この方法でも、inputElementの型をHTMLInputElementとすることができるので、コンパイラの上のエラーは起こりません。
ですがこの方法はその要素が存在しなかった(nullが返却される)場合の考慮が抜けています。
型アサーションするとコンパイラ上のエラーは消えますが、querySelectorの結果がnullであった場合でも、型アサーションによりそのチェックをスキップしてしまいます。
その結果、inputElement.valueの部分でCannot read property 'value' of nullという実行時エラーが発生します。
const inputElement = document.querySelector('#hoge') as HTMLInputElement;
const value = inputElement.value; // エラーは起きない
console.log(value); // 実行時エラー:Cannot read property 'value' of null
それに対し、ジェネリクスを使用して型を指定した場合は返り値の型は HTMLInputElemet | null で型推論されます。
そのため、コンパイラ上のエラーを解消するためには明示的なnullチェックが必要になります。
const inputElement = document.querySelector<HTMLInputElement>('#hoge');
if (!inputElement) return; // nullチェック
const value = inputElement.value;
console.log(value);
上記コードは!inputElement で要素が存在しなかった場合returnしています。
inputElementがnullでない場合にのみinputElement.valueにアクセスします。したがって、inputElementがnullだった場合でも実行時エラーは発生しません。
結論、要素取得の型指定の際はasキーワードを用いた型アサーションはできるだけ使用せず、ジェネリクスで指定するのをおすすめします。
まとめ
今回紹介したエラーはTypeScriptを使用し始めた人は一度は見るのではないでしょうか。
意味とその理解のための予備知識を知っておくと同じエラーに出会った時に役立つと思います。
