Volar (Vue 3 + TypeScript) に @types/react が混ざると型エラーになる現象と回避策
概要
本記事は、Volar を使って型安全に Vue 3 + TypeScript を書いていたら急に以下の型エラーが発生した問題の調査記録になります。
Type '{ class: string; }' is not assignable to type 'DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'.
Object literal may only specify known properties, and 'class' does not exist in type 'DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'.
TL;DR
結論から言うと、以下ディスカッションの内容の通りです。
JSX issues in template
-
Volar
はVue
コンポーネントの型チェックにJSX
を使用している - 本来は
@vue/runtime-dom
に定義されている型を利用している -
@types/react
が依存関係に入ると、JSX
ネームスペースがReact
用ので上書きされてしまう -
React
のJSX
定義にはclass
がなくclassName
で置き換えられているため、Vue
と互換性がなく型エラーになる
この問題は大抵の場合、tsconfig.json
の compilerOptions.type
フィールドを明示することで解決できます。
と、これで記事が終わっても良いんですが、意外とハマりやすそうなポイントなのに日本語の情報は無いなと思ったので、整理してまとめることにしました。
Volar
について
Volar
は、vscode
拡張を中心とした Vue.js
向けのツールセットです。主に .vue
ファイル向けの言語サーバーの提供や型チェック機能を含んでおり、 Vue 3
+ TypeScript
で開発する上では実質必須のツールです。
「Vue
は TypeScript
との相性が悪い」「Vue
は型安全性が低い」というイメージも今は昔。Volar
の力もあり、現代の Vue
は完全に型安全で使えるようになりました。
しかし、今回意図せず <template>
内の <div class="card">
というシンプルなテンプレートで div に class なんてフィールドないぞ というエラーが発生しました。
状況再現
Vite
を用いて Vue 3
+ TypeScript
のプロジェクトを作成します。
$ yarn create vite 20221227 --template vue-ts
$ cd 20221227
$ yarn install
スキャフォルドされたプロジェクトでは vscode
の推奨拡張機能が定義されているので従ってインストールしておきます。 (普段からしてるけど)
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}
HelloWorld.vue
が用意されているので確認すると、テンプレートまですべて型安全に扱える状態で、型チェックが通っていることが確認できます。
おもむろに @types/react
を追加します。
$ yarn add -D @types/react
あらふしぎ、先程まで通っていた型チェックが通らなくなりました。
以下のエラーメッセージが表示されています。
Type '{ class: string; }' is not assignable to type 'DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'.
Object literal may only specify known properties, and 'class' does not exist in type 'DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'.
何が起こっているのか
そもそも Volar
は、 <template>
の型チェックに JSX
を使用しています。
これは Vue
が Single File Component (.vue)
形式と、関数形式それぞれでコンポーネントを定義できるため、どちらにも共通の仕組みで型チェックできるようにするためです。
通常は @vue/runtime-dom
で定義されている JSX.IntrinsicElements
を使用します。
JSX.IntrinsicElements
は NativeElements
を継承しており、div
など通常の HTML 仕様に近いインタフェースを提供します。
div
要素は HTMLAttributes
インタフェースを満たしているため、<div class='card'>
は正しい型であると判断されます。
ここに @react/types
がインストールされるとどうでしょうか。 @react/types
も同様にトップレベルで JSX.IntrinsicElements
が定義されています。
React
を一度でも書いたことがあればご存じでしょうが、React
における JSX
には class
がなく、 className
になっています。
今回の場合は React
のほうの JSX.IntrinsicElements
が優先されてしまったのが原因のようです。
解決策
以下ディスカッションにて、いくつかの解決策があげられているので、それぞれ思考停止で取り入れる前に深堀りしてみます。
@types/react
を自動で読み込まないようにする
tsconfig.json
で以下のように、 compilerOptions.types
フィールドを明示的に設定します。
{
"compilerOptions": {
// ...
"types": [
"vite/client", // if using vite
// ...
]
}
}
tsconfig
における compilerOptions.types
フィールドは、どの型パッケージをグローバルスコープに読み込むかを決定します。
デフォルトでは node_modules
以下にあるすべての @types
パッケージを対象とするため、 @types/react
も読み込まれてしまいます。
それを上記のように、明示的に指定することで、それだけを読み込むように変更でき、@types/react
が勝手に読み込まれなくなります。
この方法は、 @types/react
を読み込んでいるコードがプロジェクトに存在しない場合のみ有効です。 (node_modules
以下にあるけど使われてはいない場合)
@types/react
を読み込んでいるモジュールを型チェックしない
本事象は、特に Storybook
で発生することが多いようです。
最近の例だと Storybook 7.0.0-beta.17
では、アドオン経由で @types/react
が入ってきてしまいました。
$ yarn why @types/react
=> Found "@types/react@18.0.26"
info Has been hoisted to "@types/react"
info Reasons this module exists
- Specified in "devDependencies"
- Hoisted from "@storybook#addon-essentials#@storybook#addon-docs#@mdx-js#react#@types#react"
この場合、前述の tsconfig.json
の対応をしていても、プロジェクト内に以下のようなコードが出現したら終わりです。
import { ArgsTable } from "@storybook/addon-docs";
読み込んだモジュール経由で @types/react
が読み込まれてエラーが再発してしまいます。
この問題に対して、ディスカッションでは、 tsconfig.json
の exclude
オプションを使用することが提案されています。
今回の場合は Storybook
用のコードなので、 *.stories.ts
を型チェックの対象外にします。
{
// ...
"exclude": ["**/*.stories.ts"]
}
これで型チェック時に @storybook/addon-docs
から対象外になるため、@types/react
が読み込まれなくなり、エラーが解消します。
とはいえ、これをしてしまうと Storybook
のコードに型チェックがかけられなくなるので、本末転倒でもあります。
@types/react
を改変する
@types/react
をどうしても読み込んでしまうなら、それを空パッケージで差し替えたろうという力技です。
まず、package.json
に以下を追加します。
"resolutions": {
"@types/react": "file:stub/types__react"
},
package.json
の resolutions
では、依存ツリー内に含まれる任意のパッケージの解決方法を強引に指定できます。
そしてパッケージの解決は npm
に公開されているものでなく、ファイルシステムから直接参照することも出来ます。
よって、 stub/types__react
に、何も定義しない空パッケージを作成することで、JSX
ネームスペースが上書きされないようにできます。
空パッケージの内容は以下のとおりです。
{
"name": "@types/react",
"version": "0.0.0"
}
export {}
ちなみに自社でハマったときはこの方法で対応してしまいました。
まとめ
本記事では、Volar を使ってるのにテンプレートで型エラーが発生する問題の深堀りをしました。
意外な落とし穴で、そこまでハマる機会はありませんが、あえて深堀りすることで OSS コードリーディングや TypeScript の理解を深める良い機会になりました。
Discussion