🕌

Vue3<script setup lang="ts">知見

2022/09/26に公開

はじめに

先日、仕事で参加させてもらっているプロジェクトでVueを3系にバージョンアップしました。
https://zenn.dev/yodaka/articles/84dc716de1d349

続いて、TypeScriptが入っていなかったので入れたのですが、なかなか<script setup lang="ts">の知見がネットになかったのでここに軽く共有できたらと思います。

前提

Vueは3.2系(<script setup>が使えるのは3.2系から)
TypeScriptは4.5.5(vue-createするとこれが入る)
webpacker...
ts-loader

有用なドキュメント集

おそらく<script setup>自体は簡単に書けるようになってすぐ慣れると思うのですが、TypeScript対応が結構癖があって困ります。
なのでこれらの公式ドキュメントをよく読むようにしましょう。

知見

Veturを切ってVolarを入れる

自分は実はPHPやGoでバックエンドもやる関係で、JetBrainsのIDE(WebStorm)を使っていて、このあたりはあまり詳しくないのですが、VSCodeのVue用プラグインにVeturというのがあり、長らくそれがデファクトスタンダードだったみたいです。
しかしこれは現在TS対応やVue3対応の難しさから開発が止まっており、メンテナンスオンリーな状態です。
さらにVue2.7と同じく2023年末でサポートを切るそうです。
https://github.com/vuejs/vetur/issues/3476

Vue3やTypeScriptを入れている場合は新しいVueプラグインVolarを使いましょう。
https://github.com/johnsoncodehk/volar

<script setup>ではコンポーネント名が指定できない

実は<script setup>ではコンポーネント名が指定できません。
https://stackoverflow.com/questions/67669820/how-to-define-component-name-in-vue3-setup-tag

例えばVue2系でやっていたnameプロパティは使えなくなります。

Label/index.vue
<script>
export default {
  name: 'Label',
  ...
}
</script>

これの何が辛いかというと、Vueのchrome拡張のdevTool上ではこのコンポーネントは全て<Index>と表示されることです。Label/index.vueもInput/index.vueも全部<Index>
バレルを使用する前提でよくこのコンポーネントファイル名(index.vue,index.tsxなど)をつけているプロジェクトは多いと思います。これは辛い。

ではどうすればいいかというと

解決策1:export defaultするscriptタグをもう一つ書く

実は同一ファイル内に<script setup>と普通の<script>は共存できます。
なのでname定義用にもうひとつscriptタグを書いてしまいましょう。

Label/index.vue
<script>
export default {
  name: 'Label',
}
</script>

<script setup lang="ts">
...
</script>

解決策2:ファイル名をコンポーネント名変える

これが一番シンプルです。

Label/Label.vue
<script setup lang="ts">
...
</script>

ただし、この解決方法はいずれVue側がdefineNameなどの関数を提供する可能性もあるのでなんとも言えないですね。。
自分のプロジェクトでは前者を選択しました。

<script setup lang=ts>の書き方あれこれ

まず、野暮なことは言わないので基本的な書き方はこの素晴らしい記事を読んでください。
https://zenn.dev/azukiazusa/articles/676d88675e4e74

ハマりどころや知見を紹介していきます。

Propsの定義の仕方

Propsの定義の仕方は2つあります。

このように型だけを渡す方法(type-based declaration)と

<script setup lang="ts">
const props = defineProps<{
  foo: string
  bar?: number
}>()
</script>

お馴染みの方法(runtime declaration)です。

<script setup lang="ts">
const props = defineProps({
  foo: { type: String, required: true },
  bar: Number
})
</script>

これらはどちらも同じように作用しますが、同時に使用することはできません。
おっと、type-based declarationの方はデフォルト値を設定できませんね。
そのような場合はこう書きます。

<script setup lang="ts">
export interface Props {
  msg?: string
  labels?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  msg: 'hello',
  labels: () => ['one', 'two']
})
</script>

これどちらがいいでしょうかというのは結構一長一短あると思っていますが、しっかりTypeScriptの表現を持って型定義したい場合はtype-based declarationの方がいいのかなと思い、こちらを採用しています。

ts-loaderに型チェックさせる場合、定義した型はPropTypesを使用して渡す必要がある

追記: これが必要なのはwebpack×ts-loaderの場合にts-loaderに型チェックをさせる場合のみ必要だということが判明しました。なぜかどこにも書いてなかった。。
後述の「Viteに乗り換えた方がいい」で提案しているような、ts-loaderにtranspileOnly: trueで型チェックさせずに、vue-tscで型チェックをする方式だと必要なくなるようです。

そして、ハマりどころですが、以下のように自分で定義した型をそのまま使うことが出来ません。

<script setup lang="ts">
interface Book {
  title: string
  author: string
  year: number
}

const props = defineProps<{
    // コンパイルエラーになる
  book: Book
  bar?: number
}>()
</script>

なので、PropTypeというVueが提供する型の型引数に渡して使う必要があります。

 <script setup lang="ts">
 interface Book {
   title: string
   author: string
   year: number
 }

 const props = defineProps<{
+  book: PropType<Book>
-  book: Book
   bar?: number
 }>()
 </script>

なお、PropTypeは<script setup>内に自動でimportされますのでimport文は必要ありません。

別ファイルからimportした型をそのままPropsに渡すことはできない

こちらを参照
https://vuejs.org/guide/typescript/composition-api.html#syntax-limitations

import { Props } from './other-file'

// 使えない
defineProps<Props>()

ただし、こう書いてあるので将来的には出来るようになるかも。

This is because Vue components are compiled in isolation and the compiler currently does not crawl imported files in order to analyze the source type. This limitation could be removed in a future release.

Viteに乗り換えた方がいい

もしwebpackを使っている既存プロジェクトにこのようにTSを入れてVue3にした方がいるならViteに乗り換えた方がいいです。
おそらくts-loaderを使っていると思いますが、以下のような問題が発生します。

ts-loader can only type check post-transform code. This doesn't align with the errors we see in IDEs or from vue-tsc, which map directly back to the source code.

ts-loaderはVue-loaderがトランスパイルしたTSに対して、型チェックをします。
これはIDE上で出るエラーと一致しません。
現にうちのプロジェクトではコンパイルエラーの行番号が合わない、IDE上では出てないエラーが出たりするなどの問題を抱えています。結構致命的なので今なんとかwebpackを剥がそうと色々やっています。

あとコンパイルと同じプロセスで型チェックが行われるから遅くなるらしいです。

なので、今後Vue×TypeScriptの開発環境はIDEで型チェック、Viteはトランスパイルするのみ、が最適解となるようです。

参考:https://vuejs.org/guide/typescript/overview.html#note-on-vue-cli-and-ts-loader

今、ぱっと思いついたんですがts-loaderのtranspileOnlyオプションを見つけたのでこれをtrueにしつつ、vue-tscで別プロセスで型チェックするようにすればwebpack、ts-loader構成でも良さそう?
いけそうなら別記事で検証します。
https://github.com/TypeStrong/ts-loader#faster-builds

追記: 上記の方法でいけました。
とりあえずwebpack×ts-loaderでやるなら、この方法が良さそうです。
ts-loaderで型チェックすると上記の問題以外に、vue-loaderが吐くtsにanyが入ってstrict: trueにできないなどの問題がありますので、Vue自体がts-loaderで型チェックを想定して作っていなさそうです。(明言はしてなさそうだけど…)
vue-tscはtsで書いたvueをそのまま型チェックするので、全ての問題が解決します。

終わりに

TypeScriptを入れたと一口に言いましたが、webpackerが入っててなかなか大変でした。入れたら入れたで先のts-loaderとwebpackの問題が出てきて、ちょっと先走ってしまったか、、という気持ちです。
まぁちゃんと理由があって、この開発やるまえに型が絶対欲しい!というやつがあったので入れるだけ入れる必要があったので後悔はしてないです。
このあとはバックエンドチームが今rubyのバージョンアップをやっているので、そのあとRailsのバージョンが上がってwebpackerを剥がしてViteRubyを入れるって感じの流れになるかなーと思ってます。
この記事を皮切りに日本語のVue3,TypeScript,script setupの記事が増えてくれると良いなーと思っています。

関係ないですが、技術好きなエンジニアとデザイナーの友達を増やしたいですー
今年は毎月技術記事を上げると決めて、普段は仕事でReact,Vue,Laravel,最近はGoをやってます。
お互い情報交換しつつ、自分はフリーランスでやっているのでよかったらお仕事回したりできればなと思ってます!
気軽にTwitterで声かけたりしてくださいー。
https://twitter.com/engineerYodaka

Discussion