Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Vue SFCのtemplateでTypeScriptの型を活用しよう

tsukkee
October 20, 2024

Vue SFCのtemplateでTypeScriptの型を活用しよう

Vue Fes Japan 2024 LT資料: https://vuefes.jp/2024/sessions/tsukkee

Vue SFCのtemplateは一見scriptとは分離されているように見えますが、Vue Language Toolsの力を借りることによってtemplate上で型のnarrowingをしたり、型推論したりすることができます。さらに、Volar Labs https://volarjs.dev/core-concepts/volar-labs/ というVSCode Extensionを使うことでどのように型が解釈されているかを確認することもできます。

そこで、上記の特徴を使って、template上で条件分岐の網羅性をチェックする方法や、defineSlotsマクロを通じてgenericなslotを定義する方法などを共有しつつ、Vue SFCのtemplate上でどのように型が解釈されているかを探ります。

tsukkee

October 20, 2024
Tweet

More Decks by tsukkee

Other Decks in Programming

Transcript

  1. 2 築谷 喬之 (つきたに たかゆき) 所属 ストックマーク株式会社 役割 自然言語処理を活用したBtoB SaaSの開発

    開発チームのマネージメント tsukkee (つっきー) 自己紹介 エディタ Vim 趣味 Vim上でVueのLanguage Serverを動かすこと
  2. SFCのtemplateでも型を活用していますか? <script setup lang="ts"> defineProps<{ input<: string }>(); </script> <template>

    <div v-if="input"> {{ input }} </div> </template> ここではstring | undefinedだけど… ここではstringになる! v-ifでTypeScriptのif文と 同様のnarrowingが行われ… はじめに
  3. Vue Language Toolsがtemplateの型もサポート Volar.jsの詳細は昨年のmizdraさんの発 表もご参照ください! https://speakerdeck.com/mizdra/vue-language- server-karasheng-mareta-volar-dot-js-to-soreg ami-meruke-neng-xing • Vue

    Language Toolsとは、VS CodeのVue Extension(Vue - Official)や vue-tsc、VueのLanguage Serverなどのツール群 • 基盤であるVolar.jsの機能を使って、templateをTypeScriptに変換し tscに連携し型チェック Vue Language Tools
  4. v-if TypeScriptのif文に変換される (<_VLSではじまる内部変数は今回は雰囲気で感じ取ってください^^;) <div v-if="input"> {{ input }} </div> if

    (<_VLS_ctx.input) { <_VLS_elementAsFunction( <_VLS_intrinsicElements.div, <_VLS_intrinsicElements.div ) ({}); ( <_VLS_ctx.input ); } 一部抜粋 & 整形 template → TypeScript
  5. v-for TypeScriptのfor文に変換される <template> <div v-for="item in items" :key="item"> {{ item

    }} </div> </template> for (const [item] of <_VLS_getVForSourceType((<_VLS_ctx.items)!)) { <_VLS_elementAsFunction( <_VLS_intrinsicElements.div, <_VLS_intrinsicElements.div ) ({key: ((item)), }); ( item ); [items,]; } 一部抜粋 & 整形 template → TypeScript
  6. コンポーネント呼び出し <script setup lang="ts"> defineProps<{ value: number }>(); defineEmits<{ update:

    [value: number] }>(); </script> <ChildComponent :value="count" @update="handleUpdate" <> 子コンポーネント 呼び出し側(抜粋) const <_VLS_0 = <_VLS_asFunctionalComponent( ChildComponent, new ChildComponent({ <<.{ 'onUpdate': {} as any }, value: ((<_VLS_ctx.count)), }) ); const <_VLS_1 = <_VLS_0( {<<.{ 'onUpdate': {} as any }, value: ((<_VLS_ctx.count)), }, <<.<_VLS_functionalComponentArgsRest(<_VLS_0) ); let <_VLS_5!: <_VLS_FunctionalComponentProps<typeof <_VLS_0, typeof <_VLS_1>; const <_VLS_6: Record<string, unknown> & ( </ 略 ) = { onUpdate: (<_VLS_ctx.handleUpdate) }; 一部抜粋 & 整形 大雑把には、propsとemitsを引数とする関数呼び出しに変換される template → TypeScript
  7. template上でExhaustive checkできる! <script setup lang="ts"> defineProps<{ check: never }>(); </script>

    never型を受け取るだけのExhaustiveCheck.vueを用意して… template上でExhaustive check
  8. <script setup lang="ts"> import ExhaustiveCheck from './ExhaustiveCheck.vue'; type Card =

    | { type: 'A'; id: number; name: string; } | { type: 'B'; id: number; label: string; } | { type: 'C'; id: number; title: string; }; const cards: Card[] = [ <* 略 </ ]; </script> <template> <div v-for="card in cards" :key="card.id"> <div v-if="card.type <<= 'A'"> {{ card.name }} </div> <div v-else-if="card.type <<= 'B'"> {{ card.label }} </div> <div v-else-if="card.type <<= 'C'"> {{ card.title }} </div> <div v-else> <ExhaustiveCheck :check="card" <> </div> </div> </template> template上でExhaustive check v-if…v-else-if…v-elseの最後に置いておくと… ココ!
  9. スロット (1/2) スロット定義からスロット引数の型が推論される <script setup lang="ts"> import { ref }

    from 'vue'; const value = ref(0); </script> <template> <div> <slot name="option" :slotParam="value" /> </div> </template> var <_VLS_0 = { slotParam: ((<_VLS_ctx.value)), }; </ 略 var <_VLS_slots!:{ option?(_: typeof <_VLS_0): any, }; 一部抜粋 & 整形 optionスロットは number型の slotParamを スロット引数に取る template → TypeScript
  10. スロット (2/2) defineSlotsで明示的に型を指定することもできる <script setup lang="ts"> import { ref }

    from 'vue'; defineSlots<{ option: { slotParam: number } }>(); const value = ref(0); </script> <template> <div> <slot name="option" :slotParam="value" /> </div> </template> const <_VLS_slots = defineSlots<{ option: { slotParam: number } }>() </ 略 <_VLS_normalizeSlot(<_VLS_slots['option'])<.({ slotParam: ((<_VLS_ctx.value)), }); 一部抜粋 & 整形 template → TypeScript
  11. <script setup lang="ts" generic="T extends Record<string, unknown>"> const props =

    defineProps<{ tabs: T }>(); defineSlots<{ [K in keyof T as K extends string ? `tab-${K}` : never]: (_: { value: T[K] }) <> unknown }>(); </script> <template> <div v-for="(tab, key) in tabs" :key="key"> <slot :name="`tab-${key}`" :value="tab as T[string]" /> </div> </template> genericも使うと動的なスコープ付きスロットの型付けもできる! defineSlots & generic genericでtabsの 型を受け取って、 `tab-${K}`という 命名規則でスロットの 型に展開し、 template上に反映する 例: タブコンポーネントを作りたい (けど、タブ切り替えロジックなどは省略 )
  12. こんな感じで使える! <script setup lang="ts"> import MyTabs from './MyTabs.vue'; const tabs

    = { counter: { title: 'count', count: 1234 }, image: { label: 'JPG image', image: 'image.jpg' }, }; </script> <template> <MyTabs :tabs> <template #tab-counter="{ value }"> <h1>{{ value.title }}</h1> <div>{{ value.count }}</div> <div>{{ value.label }}</div> </template> <template #tab-image="{ value }"> <div>{{ value.label }}</div> <img :src="value.image" <> </template> <template #tab-hoge="{ value }"></template> </MyTabs> </template> value: { title: string; count: number; } 存在しない プロパティは型エラー value: { label: string; image: string; } 存在しない スロットも型エラー 「tab-${キー名}」 というスコープ付き スロットを生成 defineSlots & generic