Vue.jsコンポーネントのpropsにHTMLElementを型付する

Vue.jsのコンポーネントのpropsにHTMLElementを型付しようとしてハマまりました。
やろうとしたことは

```vue
props: {
  element: {type: Object as PropType<HTMLElement>, required: true}
}
```
と記述するとTypescriptではエラーが出ないが、Vueのランタイムエラーで
```shell
[Vue warn]: Invalid prop: type check failed for prop "element". Expected Object, got HTMLDivElement
```
となるという現象でした。（※親コンポーネントで`HTMLDivElement`を取得する処理を書いてます。）

動作環境は以下の通りです。
- Vue: 2.7.14
- Typescript: 4.6.2


## 解決方法

Vue.jsコンポーネントのpropsにHTMLElementを型付する方法です。    
PropTypeを使用せず、直接HTMLELementを指定することでランタイムエラーも回避することができました。

```vue
props: {
  element: {type: HTMLElement, required: true}
}
```

## ランタイムエラーになるのはなぜか
[vuejs/vue at v2.7.14](https://github.com/vuejs/vue/tree/v2.7.14)のソースコードを読んで原因を探ります。

こちらのエラーメッセージを生成している箇所を探すと
```shell
[Vue warn]: Invalid prop: type check failed for prop "element". Expected Object, got HTMLDivElement
```

[vue/src/core/util/props.ts#L215-L217 at v2.7.14 · vuejs/vue](https://github.com/vuejs/vue/blob/v2.7.14/src/core/util/props.ts#L215-L217)
![スクリーンショット 2023-08-31 10.45.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64171/ddc6b18c-b1d2-2bd4-f48d-85916b8b0206.png)

`function getInvalidTypeMessage` の呼び出し元はこちらでした。

[vue/src/core/util/props.ts#L135 at v2.7.14 · vuejs/vue](https://github.com/vuejs/vue/blob/v2.7.14/src/core/util/props.ts#L135)
![スクリーンショット 2023-08-31 10.47.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64171/9d29de57-4b93-8375-dbf2-58d4f1383a42.png)

呼び出し元のfunction名を見てみると `function assertProp` なので、prop関連についての処理を行ってそうです。

[vue/src/core/util/props.ts#L105-L111 at v2.7.14 · vuejs/vue](https://github.com/vuejs/vue/blob/v2.7.14/src/core/util/props.ts#L105-L111)
![スクリーンショット 2023-08-31 10.52.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64171/e8355c12-2f0a-e2a7-e0c2-54399fa97fcc.png)

そのまま読み進めてみると、`assertType` を呼び出して型チェックをしていそうな箇所に行き着きました。
[vue/src/core/util/props.ts#L126-L130 at v2.7.14 · vuejs/vue](https://github.com/vuejs/vue/blob/v2.7.14/src/core/util/props.ts#L126-L130)
![スクリーンショット 2023-08-31 10.48.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64171/697e4654-31a3-e6b0-bc92-bac91a2c8c16.png)

`function assertType` を読んでみると、期待する型がObjectの場合、`isPlainObject` を呼び出して実際に渡された値がObjectかどうかの確認をしていそうです。
[vue/src/core/util/props.ts#L168-L169 at v2.7.14 · vuejs/vue](https://github.com/vuejs/vue/blob/v2.7.14/src/core/util/props.ts#L168-L169)
![スクリーンショット 2023-08-31 10.54.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64171/c7b4c385-194d-5dc5-4f84-e42bc92ace82.png)

`function isPlainObject` を見てみます。
[vue/src/shared/util.ts#L58-L64 at main · vuejs/vue](https://github.com/vuejs/vue/blob/main/src/shared/util.ts#L58-L64)
![スクリーンショット 2023-08-31 10.56.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64171/7fdcb800-7c21-d61a-ac22-4daa9552c03b.png)

`_toString.call(obj)` の部分を見ていきます。

callメソッドについて調べてみると
> call() はあるオブジェクトに所属する関数やメソッドを、別なオブジェクトに割り当てて呼び出すことができます。
>
> call() は関数やメソッドに this の新しい値を提供します。 call() によって、いったんメソッドを書いてから、新しいオブジェクトへメソッドを書き直さずに他のオブジェクトへと継承することができます。
> - 参照: [Function.prototype.call() - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Function/call)

`obj` に対して `_toString` を呼び出しているようです。  


`_toString` を見てみると `Object.prototype.toString` を呼び出しています。
[vue/src/shared/util.ts#L49-L52 at main · vuejs/vue](https://github.com/vuejs/vue/blob/main/src/shared/util.ts#L49-L52)
![スクリーンショット 2023-08-31 10.59.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64171/53d7ae72-c93c-6011-f307-79fa61afde9a.png)

`isPlainObject` に戻ってみると、引数に対しtoString()を実行し、オブジェクトかどうかを判定しているという処理でした。
[vue/src/shared/util.ts#L58-L64 at main · vuejs/vue](https://github.com/vuejs/vue/blob/main/src/shared/util.ts#L58-L64)
![スクリーンショット 2023-08-31 10.56.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64171/7fdcb800-7c21-d61a-ac22-4daa9552c03b.png)


今回の場合は、親コンポーネントで取得した `HTMLDivElement` をtoStirngした場合に、`[object Object]` となるのを期待してるようです。
実際に確認してみると `[object HTMLDivElement]` だったので、 Objectとしては判定されずエラーになっていました。

## 解決方法でエラーが出ない理由
今回の場合は

```vue
props: {
  element: {type: HTMLElement, required: true}
}
```


`function assertType`　内の `isPlainObject` より下に型をチェックする処理がありました。
[vue/src/core/util/props.ts#L173-L175 at v2.7.14 · vuejs/vue](https://github.com/vuejs/vue/blob/v2.7.14/src/core/util/props.ts#L173-L175)
![スクリーンショット 2023-08-31 11.22.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64171/a33d0b3b-2bb0-ee8a-72dd-aa962cf9389e.png)

つまり、`element instanceof HTMLElement` が `true` となるのでエラーが出なくなったということでした。


# 参考

- [vuejs/vue: This is the repo for Vue 2. For Vue 3, go to https://github.com/vuejs/core](https://github.com/vuejs/vue)
- [Options API で TypeScript を使用する | Vue.js](https://ja.vuejs.org/guide/typescript/options-api.html#typing-component-props)