🚀

vue-property-decorator + TypeScriptで書かれたVueコンポーネントをscript setupで書き換える

2022/06/06に公開

Nuxt3のstable版がリリース間近(2022/06/06現在)ということで、Nuxt.jsでもVue3で書くのがスタンダードになります。
Vue2まではvue-class-componentやvue-property-decoratorを使って、コンポーネントをclassにしてTypeScriptで書くことができていました。
Vue3ではComposition APIによってTypeScriptで記述できるようになっています。
さらにVue.js 3.2 からは <script setup> 構文が使えるようになりました。これによりComposition API を簡潔に記述できるようになります。

ここでは、vue-property-decoratorで書かれたVueコンポーネントを script setup の構文に書き換えるにあたって、それぞれの記法がどう対応しているのかを見ていきたいと思います。

概観

全体像は以下のようになります。

<template>
  <button type="button" @click="handleClick">
    {{ text }}
    <IconButton />
  </button>
</template>

<script lang="ts">
import { Component, Vue, Prop, Emit } from 'vue-property-decorator'
import IconButton from '@/components/IconButton.vue'

@Component({
  components: {
    IconButton,
  },
})
export default class ButtonPrimary extends Vue {
  @Prop({ type: String, required: true })
  private readonly text!: string

  @Emit('clickButton')
  handleClick() {}
}
</script>
<template>
  <button type="button" @click="handleClick">
    {{ text }}
    <IconButton />
  </button>
</template>

<script lang="ts" setup>
import IconButton from '@/components/IconButton.vue'

interface Props {
  text: string
}
defineProps<Props>()

defineEmits<{
  (e: 'click-button'): void
}>()
</script>

以下個別に見ていきます。

@Component

class component

<template>
  <div>
    <ChildComponent1 />
    <ChildComponent2 />
  </div>
</template>

<script lang="ts">
import ChildComponent1 from './ChildComponent1.vue'
import ChildComponent2 from './ChildComponent2.vue'

@Component({
  components: {
    ChildComponent1,
    ChildComponent2,
  },
})
export default class MyComponent extends Vue {

}
</script>

script setup

<template>
  <div>
    <ChildComponent1 />
    <ChildComponent2 />
  </div>
</template>

<script setup lang="ts">
import ChildComponent1 from './ChildComponent1.vue'
import ChildComponent2 from './ChildComponent2.vue'
</script>

import文を書くだけでコンポーネントを使用できるようになりました。

Data

class component

<script lang="ts">
@Component
export default class MyComponent extends Vue {
  message = 'Hello'

  onInput(value: string) {
    this.message = value
  }
}
</script>

script setup

<script setup lang="ts">
const message = ref('Hello')

const onInput = (value: string) => {
  message.value = value
}
</script>

ref で宣言することでreactiveな値として使用できるようになります。
値にアクセスするには .value をつける必要があります。

@Prop

class component

<script lang="ts">
@Component
export default class MyComponent extends Vue {
  @Prop({ type: String, required: true })
  private readonly text!: string

  // default値
  @Prop({ type: Number, default: 2 })
  private readonly num!: number

  // Object
  @Prop({ type: Object, required: true })
  private readonly myObj!: MyObj

}
</script>

script setup

<script setup lang="ts">
interface Props {
  text: string
  num?: number
  myObj: MyObj
}
const props = withDefaults(defineProps<Props>(), {
  num: 2,
})

</script>

独自型もきちんと型を付けられるようになり、コンポーネントを利用する側でも補完が効くようになります。

@Emit

class component

<script lang="ts">
@Component
export default class MyComponent extends Vue {
  @Emit('clickButton')
  handleClick(num: number) {
    return num
  }

}
</script>

script setup

<script setup lang="ts">
const emits = defineEmits<{
  (e:'click-button', num: number): void
}>()

const handleClick = (num: number) => {
  emits('click-button', num)
}
</script>

@Watch

class component

<script lang="ts">
@Component
export default class MyComponent extends Vue {
  checkBox: boolean = false

  @Watch('checkBox')
  watchValue(value: boolean) {
    if (value) {
        alert('checked!!')
    }
  }
}
</script>

script setup

<script setup lang="ts">
const checkBox = ref(false)

watch(
  checkBox,
  (value: boolean) => {
    if (value) {
        alert('checked!!')
    }
  }
)
</script>

@Ref

class component

<template>
  <ChildComponent ref="childComponent" />
  <button ref="submitButton">Submit</button>
</template>

<script lang="ts">
import ChildComponent from './ChildComponent.vue'

@Component
export default class MyComponent extends Vue {
  @Ref() readonly childComponent!: ChildComponent
  @Ref('submitButton') readonly button!: HTMLButtonElement
}
</script>

script setup

<template>
  <ChildComponent ref="childComponent" />
  <button ref="submitButton">Submit</button>
</template>

<script setup lang="ts">
const childComponent = ref<ChildComponent>()
const submitButton = ref<HTMLButtonElement>()
</script>

同じ変数名のrefで宣言するだけで参照できるようになりました

Computed

class component

<script lang="ts">
@Component
export default class MyComponent extends Vue {
  firstName = 'John'
  lastName = 'Doe'

  get name() {
    return this.firstName + ' ' + this.lastName
  }

  set name(value) {
    const splitted = value.split(' ')
    this.firstName = splitted[0]
    this.lastName = splitted[1] || ''
  }
}
</script>

script setup

<script setup lang="ts">
const firstName = ref('John')
const lastName = ref('Doe')

const name = computed({
  get: () =>  firstName + ' ' + lastName,
  set: (value) => {
    const splitted = value.split(' ')
    firstName.value = splitted[0]
    lastName.value = splitted[1] || ''
  }
})

</script>

Hooks

class component

<script lang="ts">
@Component
export default class MyComponent extends Vue {
  mounted() {
    console.log('mounted')
  }
}
</script>

script setup

<script setup lang="ts">
onMounted(() => {
  console.log('mounted')
})
</script>

Vue2 -> 3でのライフサイクルフックの変更についてはこちらを参照
ライフサイクルフック | Vue.js

  • setup は beforeCreate と created のライフサイクルで実行されるため、これらのフック内で実行していたコードはsetup内に直接書く
  • mounted -> onMounted のように、onXXXXXという名前に変わった

Discussion