如何爲 Vue3 組件標註 TS 類型,看這個就夠了!

1 開篇

原文作者說是 2022 年也就是今年,Vue3 和 TS 是最熱門的前端技術,其實去年就已經很火了。

2 文章開始的地方

今天就給大家分享一下如何在 Vue3 組件中結合Composition-Api使用 TS 類型。如果有不會或者不熟的小夥伴,一起學起來吧!

3 爲props標註類型

使用 <script setup>

當使用<script setup>時,defineProps()宏函數支持從它的參數中推導類型:

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

props.foo // string
props.bar // number | undefined
</script>

這被稱爲運行時聲明,因爲傳遞給defineProps()的參數會作爲運行時的 props 選項使用。

第二種方式,通過泛型參數來定義 props 的類型,這種方式更加直接:

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

// or
<script setup lang="ts">
interface Props {
  foo: string
  bar?: number
}

const props = defineProps<Props>()
</script>

這被稱爲基於類型的聲明,編譯器會盡可能地嘗試根據類型參數推導出等價的運行時選項。這種方式的不足之處在於,失去了定義 props 默認值的能力。爲了解決這個問題,我們可以使用 withDefaults 編譯器宏:

interface Props {
  msg?: string
  labels?: string[]
}

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

上面代碼會被編譯爲等價的運行時 props 的 default 選項。

<script setup>

如果沒有使用 <script setup>,那麼爲了開啓 props 的類型推導,必須使用 defineComponent()。傳入 setup()props 對象類型是從 props 選項中推導而來。

import { defineComponent } from 'vue'

export default defineComponent({
  props: {
    message: String
  },
  setup(props) {
    props.message // <-- 類型:string
  }
})

4 爲 emits 標註類型

使用 <script setup>

<script setup> 中,emit 函數的類型標註也可以使用 運行時聲明 或者 基於類型的聲明 :

<script setup lang="ts">
// 運行時
const emit = defineEmits(['change''update'])

// 基於類型
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
</script>

我們可以看到,基於類型的聲明 可以使我們對所觸發事件的類型進行更細粒度的控制。

<script setup>

若沒有使用 <script setup>defineComponent() 也可以根據 emits 選項推導暴露在 setup 上下文中的 emit 函數的類型:

import { defineComponent } from 'vue'

export default defineComponent({
  emits: ['change'],
  setup(props, { emit }) {
    emit('change') // <-- 類型檢查 / 自動補全
  }
})

5 爲 ref() 標註類型

默認推導類型

ref 會根據初始化時的值自動推導其類型:

import { ref } from 'vue'

// 推導出的類型:Ref<number>
const year = ref(2020)

// => TS Error: Type 'string' is not assignable to type 'number'.
year.value = '2020'

通過接口指定類型 有時我們可能想爲 ref 內的值指定一個更復雜的類型,可以使用 Ref 這個接口:

import { ref } from 'vue'
import type { Ref } from 'vue'

const year: Ref<string | number> = ref('2020')

year.value = 2020 // 成功!

通過泛型指定類型

或者,在調用 ref() 時傳入一個泛型參數,來覆蓋默認的推導行爲:

// 得到的類型:Ref<string | number>
const year = ref<string | number>('2020')

year.value = 2020 // 成功!

如果你指定了一個泛型參數但沒有給出初始值,那麼最後得到的就將是一個包含 undefined 的聯合類型:

// 推導得到的類型:Ref<number | undefined>
const n = ref<number>()

6 爲 reactive() 標註類型

默認推導類型

reactive() 也會隱式地從它的參數中推導類型:

import { reactive } from 'vue'

// 推導得到的類型:{ title: string }
const book = reactive({ title: 'Vue 3 指引' })

通過接口指定類型

要顯式地指定一個 reactive 變量的類型,我們可以使用接口:

import { reactive } from 'vue'

interface Book {
  title: string
  year?: number
}

const book: Book = reactive({ title: 'Vue 3 指引' })

7 爲 computed() 標註類型

默認推導類型

computed() 會自動從其計算函數的返回值上推導出類型:

import { ref, computed } from 'vue'

const count = ref(0)

// 推導得到的類型:ComputedRef<number>
const double = computed(() => count.value * 2)

// => TS Error: Property 'split' does not exist on type 'number'
const result = double.value.split('')

通過泛型指定類型

你還可以通過泛型參數顯式指定類型:

const double = computed<number>(() ={
  // 若返回值不是 number 類型則會報錯
})

8 爲事件處理函數標註類型

在處理原生 DOM 事件時,應該給事件處理函數的參數正確地標註類型。讓我們看一下這個例子:

<script setup lang="ts">
function handleChange(event) {
  // `event` 隱式地標註爲 `any` 類型
  console.log(event.target.value)
}
</script>

<template>
  <input type="text" @change="handleChange" />
</template>

沒有類型標註時,這個 event 參數會隱式地標註爲 any 類型。這也會在 tsconfig.json 中配置了 "strict": true"noImplicitAny": true 時報出一個 TS 錯誤。因此,建議顯式地爲事件處理函數的參數標註類型。此外,你可能需要顯式地強制轉換 event 上的屬性:

function handleChange(event: Event) {
  console.log((event.target as HTMLInputElement).value)
}

9 爲 provide / inject 標註類型

provideinject 通常會在不同的組件中運行。要正確地爲注入的值標記類型,Vue 提供了一個 InjectionKey 接口,它是一個繼承自 Symbol 的泛型類型,可以用來在提供者和消費者之間同步注入值的類型:

import { provide, inject } from 'vue'
import type { InjectionKey } from 'vue'

const key = Symbol() as InjectionKey<string>

provide(key, 'foo') // 若提供的是非字符串值會導致錯誤

const foo = inject(key) // foo 的類型:string | undefined

建議將注入 key 的類型放在一個單獨的文件中,這樣它就可以被多個組件導入。當使用字符串注入 key 時,注入值的類型是 unknown,需要通過泛型參數顯式聲明:

const foo = inject<string>('key') // 類型:string | undefined

注意注入的值仍然可以是 undefined,因爲無法保證提供者一定會在運行時 provide 這個值。當提供了一個默認值後,這個 undefined 類型就可以被移除:

const foo = inject<string>('foo''bar') // 類型:string

如果你確定該值將始終被提供,則還可以強制轉換該值:

const foo = inject('foo') as string

10 爲 dom 模板引用標註類型

模板 ref 需要通過一個顯式指定的泛型參數和一個初始值 null 來創建:

<script setup lang="ts">
import { ref, onMounted } from 'vue'

const el = ref<HTMLInputElement | null>(null)

onMounted(() ={
  el.value?.focus()
})
</script>

<template>
  <input ref="el" />
</template>

注意爲了嚴格的類型安全,有必要在訪問 el.value 時使用可選鏈或類型守衛。這是因爲直到組件被掛載前,這個 ref 的值都是初始的 null,並且 v-if 將引用的元素卸載時也會被設置爲 null

11 爲組件模板引用標註類型

有時,我們需要爲一個子組件添加一個模板 ref,以便調用它公開的方法。比如,我們有一個 MyModal 子組件,它有一個打開模態框的方法:

<!-- MyModal.vue -->
<script setup lang="ts">
import { ref } from 'vue'

const isContentShown = ref(false)
const open = () =(isContentShown.value = true)

defineExpose({
  open
})
</script>

爲了獲取 MyModal 的類型,我們首先需要通過 typeof 得到其類型,再使用 TypeScript 內置的InstanceType工具類型來獲取其實例類型:

<!-- App.vue -->
<script setup lang="ts">
import MyModal from './MyModal.vue'

const modal = ref<InstanceType<typeof MyModal> | null>(null)

const openModal = () ={
  modal.value?.open()
}
</script>

作者:前端阿飛

來源:https://juejin.cn/post/7129130323148800031

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Fpq8SOgRutbLpGGr6OqBlw