【TypeScript】プロパティ 'hoge' は型 'Fuga' に存在しませんのエラーを考える

## 前置き
今年の4月からTypeScriptを使用する現場で働いています。
実務でTypeScriptを使用したときに、最初にぶつかったのがタイトルのエラーでした。
本記事ではこのエラーはどういう意味なのか、理解のために必要なことをまとめます。

## 要素はそれぞれ独自のDOMインターフェースを持っている

### プロパティ 'hoge' は型 'Fuga' に存在しません

例：プロパティ 'value' は型 'Element' に存在しません。

```tsx
const inputElement = document.querySelector('#hoge');
if (!inputElement) return;
const value = inputElement.value; // エラー：プロパティ 'value' は型 'Element' に存在しません。
```
このようなエラーに何度も遭遇しました。
`document.querySelector()` で要素を取得すると`Element`型が返却されますが、`value`プロパティは`Element`型には存在しないため、使用できませんというエラーです。

要素はそれぞれ独自のインターフェースを持っており、上記のエラーを解消するためには`value`プロパティを持つ要素だということをTypeScriptに伝える必要があります。


例のコードはinput要素を取得しています。
input要素のDOMインターフェースの型を、以下のようにジェネリクスを使用して戻り値の型をより具体的に指定するとエラーが解消されました！

```tsx

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を追加・削除するためのメソッドが定義されています。

![ツリー構造.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2322267/63970abd-5d28-beb4-ba9e-df66dd5c3b31.png)


#### スーパータイプとサブタイプ

「スーパータイプ」と「サブタイプ」は、主にオブジェクト指向プログラミングにおける型の階層構造を表すための用語です。


**スーパータイプ（または親型、基底型）**
一つ以上の「サブタイプ」（または子型、派生型）を持つことができる型を指します。
スーパータイプは、そのサブタイプが共通に持つべき特性や振る舞いを定義します。


**サブタイプ（または子型、派生型）**
スーパータイプから特性や振る舞いを**継承**し、それに追加や特化を行った新しい型を指します。
サブタイプはスーパータイプの特性や振る舞いを「再利用」することで、同じコードを何度も書くことなく新しい機能を提供できます。

図示するとこのようになります

```tsx
Node (スーパータイプ)
  |
  +---- Element (サブタイプ | スーパータイプ)
  |      |
  |      +---- HTMLElement (サブタイプ | スーパータイプ)
  |      |      |
  |      |      +---- HTMLDivElement (サブタイプ)
  |      |      |
  |      |      +---- HTMLAnchorElement (サブタイプ)
  |      |      |
  |      |      +---- HTMLInputElement (サブタイプ)
  |      |
  |      +---- SVGElement (サブタイプ)
  |
  +---- CharacterData (サブタイプ | スーパータイプ)
         |
         +---- Text (サブタイプ)
         |
         +---- Comment (サブタイプ)
```

`Node`は`Element`や`CharacterData`などのスーパータイプとなります。

さらに、`Element`は`HTMLElement`と`SVGElement`のスーパータイプで、これらもまたそれぞれのサブタイプ（`HTMLDivElement`、`HTMLAnchorElement`、`HTMLInputElement`など）を持っています。

これらのサブタイプはスーパータイプから特性や振る舞いを継承し、その上で自身の特性や振る舞いを追加しています。

上部で取り扱った例をもう一度振り返ってみましょう。

```tsx
const inputElement = document.querySelector('#hoge');
```

ここで取得した要素の型は `Element` なので、`Node`と、`Element`のインターフェースが使用できます。
ですが、まだ取得した要素が `HTMLInputElement` だと指定していないので、`HTMLInputElement`インターフェースは使用できません。

```tsx
// NodeのプロパティであるtextContentが使用できる
inputElement.textContent = 'hoge';

// ElementのプロパティであるclassNameが使用できる
const className = inputElement.className;

// HTMLInputElementのプロパティであるvalueは使用できない
const value = inputElement.value; // エラー：プロパティ 'value' は型 'Element' に存在しません。
```

このインターフェースのおかげで、プログラムの中でHTML要素を扱う際に、その要素が持つべき特性や振る舞いについてコンパイラがチェックしてくれます。

これは意図しない操作を未然に防ぐために有用です。

### `as`キーワードを用いた型アサーションでも同じ？

TypeScriptに型を教えるには、`as`キーワードを用いて型アサーションする方法もあります。

```tsx
const inputElement = document.querySelector('#hoge') as HTMLInputElement;
const value = inputElement.value; // エラーは起きない
```

この方法でも、inputElementの型を`HTMLInputElement`とすることができるので、コンパイラの上のエラーは起こりません。
ですがこの方法はその要素が存在しなかった(`null`が返却される)場合の考慮が抜けています。

型アサーションするとコンパイラ上のエラーは消えますが、`querySelector`の結果が`null`であった場合でも、型アサーションによりそのチェックをスキップしてしまいます。

その結果、`inputElement.value`の部分で`Cannot read property 'value' of null`という実行時エラーが発生します。

```tsx
const inputElement = document.querySelector('#hoge') as HTMLInputElement;
const value = inputElement.value; // エラーは起きない
console.log(value); // 実行時エラー：Cannot read property 'value' of null
```

それに対し、ジェネリクスを使用して型を指定した場合は返り値の型は `HTMLInputElemet | null` で型推論されます。

そのため、コンパイラ上のエラーを解消するためには明示的なnullチェックが必要になります。

```tsx
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を使用し始めた人は一度は見るのではないでしょうか。
意味とその理解のための予備知識を知っておくと同じエラーに出会った時に役立つと思います。