いがにんのぼやき

WEBエンジニアのブログ。IT、WEB、バンド、アニメ。

Vue3のtemplate型検査でできること、できないこと

6/8追記-------------------------
最新のvue-tsc 0.37.3で試したところ以下の変化があった

Props Component injection VSCode上ではエラーにならないが効く。vue-tscではエラーになる

vue-tscでもエラーにならないようになった

Slot Property VSCode上では効かない、vue-tscでは効く

VSCode上でも効くようになった

追記終わり-------------------------

Vue3において、どのくらいVueのtemplateで型をチェックできるようになったのか確認する
https://igatea.hatenablog.com/entry/2022/05/01/194916 と同じように npm init vite で生成したVue3テンプレートで検証をしていく
VSCode上でVolarを使ってエディタ上でも型検査が効きエラーメッセージが出るか、vue-tsc --noEmit を叩いて型検査が行えるかを確認する

検証項目

  • Props
    • String union
    • Function
    • Component injection
  • Emit
  • Slot
  • Slot property

基本的にVueのSFCのtemplateとscript setupで検証をする
JSXやoption api、option apiの中でのsetupでは検証していない

検証コード https://gist.github.com/igayamaguchi/650726b9e84ce9fb14817657a278c857

結論

先に結論

検査項目
Props String union 効く
Props Function 効く
Props Component injection VSCode上ではエラーにならないが効く。vue-tscではエラーになる
Emit 効く。ただし部分集合のようになっているようで引数が足りていないものはエラーにならない。未定義のイベントを設定してもエラーにならない
Slot 効かない
Slot Property VSCode上では効かない、vue-tscでは効く

なぜかVSCodeとvue-tscで若干差がある。原因は不明

Props

String union

コンポーネント

<template>
  <div>
    <h3>StringUnion</h3>
    <p>{{ theme }}</p>
  </div>
</template>

<script lang="ts">
export const themes = ['primary', 'secondary'] as const
export type Theme = typeof themes[number]
</script>
<script setup lang="ts">
defineProps<{
  theme: Theme
}>()
</script>

コンポーネント

<script setup lang="ts">
import StringUnion from './components/StringUnion.vue';
</script>

<template>
  <div>
    <h1>App</h1>

    <div>
      <h2>Props</h2>
      <!-- OK -->
      <StringUnion theme="primary" />
      <!-- NG -->
      <StringUnion theme="no-theme" />
      <!-- OK:定義していないpropsを設定していてもエラーにはならない -->
      <StringUnion theme="primary" invalid-value="primary" />
    </div>
  </div>
</template>

vue-tsc

src/App.vue:33:20 - error TS2322: Type '"no-theme"' is not assignable to type '"primary" | "secondary"'.

33       <StringUnion theme="no-theme" />
                      ~~~~~

VSCode上も、vue-tscでもしっかりString unionも型が効いた

Function

コンポーネント

<template>
  <div>
    <h3>Function</h3>
  </div>
</template>

<script lang="ts">
export type ChangeFn = (id: number) => void
</script>
<script setup lang="ts">
defineProps<{
  change: ChangeFn
}>()
</script>

コンポーネント

<script setup lang="ts">
import Function from './components/Function.vue';

const changePropsCorrect = (id: number) => {}
const changePropsIncorrect = (id: string) => {}
</script>

<template>
  <div>
    <h1>App</h1>

    <div>
      <h2>Props</h2>
      <!-- OK -->
      <Function :change="changePropsCorrect" />
      <!-- NG -->
      <Function :change="changePropsIncorrect" />
  </div>
</template>

vue-tsc

src/App.vue:40:18 - error TS2322: Type '(id: string) => void' is not assignable to type 'ChangeFn'.
  Types of parameters 'id' and 'id' are incompatible.
    Type 'number' is not assignable to type 'string'.

40       <Function :change="changePropsIncorrect" />
                    ~~~~~~

これも正しく型の検査がされている

Component injection

コンポーネント
ComponentInjection.vue

<template>
  <div>
    <h3>ComponentInjection</h3>
    <Component :is="item" />
  </div>
</template>

<script setup lang="ts">
import { DefineComponent } from 'vue';
import ComponentInjectionItem from './ComponentInjectionItem.vue';
defineProps<{
  item: typeof ComponentInjectionItem,
}>()
</script>

Propsに指定するコンポーネント
ComponentInjectionItem.vue

<template>
  <div>
    <h4>ComponentInjectionItem</h4>
    <p>{{ id }}</p>
  </div>
</template>

<script setup lang="ts">
defineProps<{
  id: number
}>()
</script>

コンポーネント

<script setup lang="ts">
import ComponentInjection from './components/ComponentInjection.vue';
import ComponentInjectionItem from './components/ComponentInjectionItem.vue';
</script>

<template>
  <div>
    <h1>App</h1>

    <div>
      <h2>Props</h2>
      <!-- NG:VSCode上はエラーにならないけどvue-tscだとエラーになる -->
      <ComponentInjection :item="ComponentInjectionItem" />
    </div>
  </div>
</template>

vue-tsc

src/App.vue:43:28 - error TS2322: Type 'DefineComponent<__VLS_DefinePropsToOptions<{ id: number; }>, {}, unknown, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, ... 4 more ..., {}>' is not assignable to type 'ComponentPublicInstanceConstructor<{ $: ComponentInternalInstance; $data: {}; $props: Partial<{}> & Omit<Readonly<ExtractPropTypes<__VLS_DefinePropsToOptions<{ id: number; }>>> & VNodeProps & AllowedComponentProps & ComponentCustomProps, never>; ... 10 more ...; $watch(source: string | Function, cb: Function, option...'.
  Type 'ComponentPublicInstanceConstructor<{ $: ComponentInternalInstance; $data: {}; $props: Partial<{}> & Omit<Readonly<ExtractPropTypes<__VLS_DefinePropsToOptions<{ id: number; }>>> & VNodeProps & AllowedComponentProps & ComponentCustomProps, never>; ... 10 more ...; $watch(source: string | Function, cb: Function, option...' is missing the following properties from type '{ __VLS_raw: DefineComponent<__VLS_DefinePropsToOptions<{ id: number; }>, {}, unknown, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, ... 4 more ..., {}>; __VLS_options: { ...; }; __VLS_slots: {}; }': __VLS_raw, __VLS_options, __VLS_slots

43       <ComponentInjection :item="ComponentInjectionItem" />
                              ~~~~

Emit

コンポーネント

<template>
  <div @click="handleClick">
    <h3>Emit</h3>
  </div>
</template>

<script setup lang="ts">
const emits = defineEmits<{
    (e: 'click-event', id: number): void
}>()
function handleClick() {
    emits('click-event', 1)
}
</script>

コンポーネント

<script setup lang="ts">
import Emit from './components/Emit.vue';

const handleChangeCorrect = (id: number) => {
  console.log(id)
}
const handleChangeInorrect = (id: string) => {
  console.log(id)
}
const handleChangeNotEnough = () => {
  console.log('not enough')
}
</script>

<template>
  <div>
    <h1>App</h1>

    <div>
      <h2>Emit</h2>
      <!-- OK -->
      <Emit @click-event="handleChangeCorrect" />
      <!-- NG -->
      <Emit @click-event="handleChangeIncorrect" />
      <!-- OK:引数が足りていない場合でもエラーにならない -->
      <Emit @click-event="handleChangeNotEnough" />
      <!-- OK:定義していないeventを設定していてもエラーにはならない -->
      <Emit @invalid-event="handleChangeCorrect" />
    </div>
  </div>
</template>

vue-tsc

src/App.vue:51:27 - error TS2552: Cannot find name 'handleChangeIncorrect'. Did you mean 'handleChangeInorrect'?

51       <Emit @click-event="handleChangeIncorrect" />
                             ~~~~~~~~~~~~~~~~~~~~~

Slot

コンポーネント

<template>
  <div>
    <h3>Slot</h3>
    <slot name="header" />
    <slot />
  </div>
</template>

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

コンポーネント

<template>
  <div>
    <h3>Slot</h3>
    <slot name="header" />
    <slot />
  </div>
</template>

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

vue-tsc

<script setup lang="ts">
import SlotComponent from './components/SlotComponent.vue';
</script>

<template>
  <div>
    <h1>App</h1>

    <div>
      <h2>Slot</h2>

      <SlotComponent>
        <template #header>
          <div>slot header</div>
        </template>
        <div>slot</div>
        <!-- エラーにはならない -->
        <template #invalid>
          <div>slot invalid</div>
        </template>
      </SlotComponent>
    </div>
  </div>
</template>

Slot property

コンポーネント

<template>
  <div>
    <h3>SlotProperty</h3>
    <slot name="header" value="header" />
    <slot :id="1" />
  </div>
</template>

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

コンポーネント

<script setup lang="ts">
import SlotProperty from './components/SlotProperty.vue';
</script>

<template>
  <div>
    <h1>App</h1>

    <div>
      <h2>Slot</h2>

      <SlotProperty v-slot="slotProps">
        <!-- OK -->
        <div>slot {{ slotProps.id }}</div>
        <!-- NG -->
        <div>slot {{ slotProps.x }}</div>
      </SlotProperty>
      <!-- slot properyに未定義のxはエラーに -->
      <SlotProperty v-slot="{ id, x }">
        <div>slot {{ id }}</div>
        <div>slot {{ x }}</div>
      </SlotProperty>

      <SlotProperty>
        <template #header="headerProps">
          <!-- OK -->
          <div>header {{ headerProps.value }}</div>
          <!-- NG -->
          <div>header {{ headerProps.x }}</div>
        </template>
        <template v-slot="slotProps">
          <!-- OK -->
          <div>slot {{ slotProps.id }}</div>
          <!-- NG -->
          <div>slot {{ slotProps.x }}</div>
        </template>
      </SlotProperty>
      <SlotProperty>
        <!-- slot properyに未定義のxはエラーに -->
        <template #header="{ value, x }">
          <div>header {{ value }}</div>
          <div>header {{ x }}</div>
        </template>
        <!-- slot properyに未定義のxはエラーに -->
        <template v-slot="{ id, x }">
          <div>slot {{ id }}</div>
          <div>slot {{ x }}</div>
        </template>
      </SlotProperty>
    </div>
  </div>
</template>

vue-tsc

src/App.vue:76:32 - error TS2339: Property 'x' does not exist on type '{ id: number; }'.

76         <div>slot {{ slotProps.x }}</div>
                                  ~

src/App.vue:79:35 - error TS2339: Property 'x' does not exist on type '{ id: number; }'.

79       <SlotProperty v-slot="{ id, x }">
                                     ~

src/App.vue:89:38 - error TS2339: Property 'x' does not exist on type '{ value: string; }'.

89           <div>header {{ headerProps.x }}</div>
                                        ~

src/App.vue:95:34 - error TS2339: Property 'x' does not exist on type '{ id: number; }'.

95           <div>slot {{ slotProps.x }}</div>
                                    ~

src/App.vue:100:37 - error TS2339: Property 'x' does not exist on type '{ value: string; }'.

100         <template #header="{ value, x }">
                                        ~

src/App.vue:105:33 - error TS2339: Property 'x' does not exist on type '{ id: number; }'.

105         <template v-slot="{ id, x }">
                                    ~