Vue3<script setup lang="ts">知見
はじめに
先日、仕事で参加させてもらっているプロジェクトでVueを3系にバージョンアップしました。
続いて、TypeScriptが入っていなかったので入れたのですが、なかなか<script setup lang="ts">
の知見がネットになかったのでここに軽く共有できたらと思います。
前提
Vueは3.2系(<script setup>
が使えるのは3.2系から)
TypeScriptは4.5.5(vue-createするとこれが入る)
webpacker...
ts-loader
有用なドキュメント集
おそらく<script setup>
自体は簡単に書けるようになってすぐ慣れると思うのですが、TypeScript対応が結構癖があって困ります。
なのでこれらの公式ドキュメントをよく読むようにしましょう。
-
SFC
<script setup>
TypeScript のみの機能
https://v3.ja.vuejs.org/api/sfc-script-setup.html#typescript-のみの機能 -
Using Vue with TypeScript(環境構築系)
https://vuejs.org/guide/typescript/overview.html#using-vue-with-typescript -
TypeScript with Composition API
https://vuejs.org/guide/typescript/composition-api.html
知見
Veturを切ってVolarを入れる
自分は実はPHPやGoでバックエンドもやる関係で、JetBrainsのIDE(WebStorm)を使っていて、このあたりはあまり詳しくないのですが、VSCodeのVue用プラグインにVeturというのがあり、長らくそれがデファクトスタンダードだったみたいです。
しかしこれは現在TS対応やVue3対応の難しさから開発が止まっており、メンテナンスオンリーな状態です。
さらにVue2.7と同じく2023年末でサポートを切るそうです。
Vue3やTypeScriptを入れている場合は新しいVueプラグインVolarを使いましょう。
<script setup>
ではコンポーネント名が指定できない
実は<script setup>
ではコンポーネント名が指定できません。
例えばVue2系でやっていたname
プロパティは使えなくなります。
<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タグを書いてしまいましょう。
<script>
export default {
name: 'Label',
}
</script>
<script setup lang="ts">
...
</script>
解決策2:ファイル名をコンポーネント名変える
これが一番シンプルです。
<script setup lang="ts">
...
</script>
ただし、この解決方法はいずれVue側がdefineNameなどの関数を提供する可能性もあるのでなんとも言えないですね。。
自分のプロジェクトでは前者を選択しました。
<script setup lang=ts>
の書き方あれこれ
まず、野暮なことは言わないので基本的な書き方はこの素晴らしい記事を読んでください。
ハマりどころや知見を紹介していきます。
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に渡すことはできない
こちらを参照
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構成でも良さそう?
いけそうなら別記事で検証します。
追記: 上記の方法でいけました。
とりあえず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で声かけたりしてくださいー。
Discussion