跳轉到內容

使用組合式API的TypeScript

此頁面假設您已經閱讀了使用TypeScript與Vue的概述。

元件屬性型別化

使用 <script setup>

當使用 <script setup> 時,defineProps() 宏支援根據其引數推斷props型別

vue
<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會更直接

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

這被稱為“型別宣告”。編譯器將盡力根據型別引數推斷等價的執行時選項。在這種情況下,我們的第二個示例編譯成了與第一個示例完全相同的執行時選項。

您可以使用型別宣告或執行時宣告中的任何一個,但不能同時使用兩個。

我們還可以將props型別移動到單獨的介面中

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

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

如果 Props 從外部源匯入,這也同樣適用。此功能要求TypeScript是Vue的依賴項。

vue
<script setup lang="ts">
import type { Props } from './foo'

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

語法限制

在版本3.2及以下版本中,defineProps()的泛型型別引數限制為型別字面或區域性介面的引用。

這個限制在3.3版本中已解決。Vue的最新版本支援在型別引數位置引用匯入的有限數量的複雜型別。然而,由於型別到執行時的轉換仍然是基於AST的,因此不支援需要實際型別分析的某些複雜型別,例如條件型別。您可以為單個prop使用條件型別,但不能為整個props物件使用。

屬性預設值

當使用基於型別的宣告時,我們失去了宣告屬性預設值的能力。可以透過使用響應式屬性解構 來解決這個問題。

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

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

在3.4及以下版本中,預設未啟用響應式屬性解構。一種替代方法是使用withDefaults編譯器宏

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

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

這將編譯為等效的執行時屬性default選項。此外,withDefaults輔助函式提供了對預設值的型別檢查,並確保返回的props型別已移除具有預設值的屬性的必需標誌。

INFO

請注意,當使用withDefaults時,可變引用型別的預設值(如陣列或物件)應使用函式包裝,以避免意外修改和外部副作用。這確保每個元件例項都獲得自己的預設值副本。在使用解構時使用預設值時,這不是必要的。

沒有<script setup>

如果不使用<script setup>,則必須使用defineComponent()來啟用屬性型別推斷。傳遞給setup()的屬性物件型別從props選項推斷。

ts
import { defineComponent } from 'vue'

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

複雜屬性型別

使用基於型別的宣告時,屬性可以使用與任何其他型別類似的複雜型別

vue
<script setup lang="ts">
interface Book {
  title: string
  author: string
  year: number
}

const props = defineProps<{
  book: Book
}>()
</script>

對於執行時宣告,我們可以使用PropType實用型別

ts
import type { PropType } from 'vue'

const props = defineProps({
  book: Object as PropType<Book>
})

如果我們直接指定props選項,它將以相同的方式工作

ts
import { defineComponent } from 'vue'
import type { PropType } from 'vue'

export default defineComponent({
  props: {
    book: Object as PropType<Book>
  }
})

props選項更常與Options API一起使用,因此您將在TypeScript與Options API的指南中找到更多詳細示例。那些示例中顯示的技術也適用於使用defineProps()進行的執行時宣告。

為元件發射進行型別定義

<script setup>中,也可以使用執行時宣告或型別宣告來對emit函式進行型別定義

vue
<script setup lang="ts">
// runtime
const emit = defineEmits(['change', 'update'])

// options based
const emit = defineEmits({
  change: (id: number) => {
    // return `true` or `false` to indicate
    // validation pass / fail
  },
  update: (value: string) => {
    // return `true` or `false` to indicate
    // validation pass / fail
  }
})

// type-based
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()

// 3.3+: alternative, more succinct syntax
const emit = defineEmits<{
  change: [id: number]
  update: [value: string]
}>()
</script>

型別引數可以是以下之一

  1. 一個可呼叫函式型別,但寫成具有呼叫簽名的型別字面量。它將用作返回的emit函式的型別。
  2. 一個型別字面量,其中鍵是事件名稱,值是表示事件額外接受引數的陣列/元組型別。上面的示例使用了命名元組,因此每個引數都可以有一個顯式的名稱。

正如我們所見,型別宣告使我們能夠對發射事件的型別約束有更細粒度的控制。

當不使用 <script setup> 時,defineComponent() 可以推斷 setup 上下文中暴露的 emit 函式允許的事件。

ts
import { defineComponent } from 'vue'

export default defineComponent({
  emits: ['change'],
  setup(props, { emit }) {
    emit('change') // <-- type check / auto-completion
  }
})

輸入 ref()

引用會從初始值推斷型別。

ts
import { ref } from 'vue'

// inferred type: Ref<number>
const year = ref(2020)

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

有時我們可能需要為引用的內部值指定複雜型別。我們可以透過使用 Ref 型別來實現這一點。

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

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

year.value = 2020 // ok!

或者,在呼叫 ref() 時傳遞泛型引數以覆蓋預設推斷。

ts
// resulting type: Ref<string | number>
const year = ref<string | number>('2020')

year.value = 2020 // ok!

如果您指定了泛型型別引數但省略了初始值,則結果型別將是一個包含 undefined 的聯合型別。

ts
// inferred type: Ref<number | undefined>
const n = ref<number>()

輸入 reactive()

reactive() 也會從其引數隱式推斷型別。

ts
import { reactive } from 'vue'

// inferred type: { title: string }
const book = reactive({ title: 'Vue 3 Guide' })

要顯式型別化 reactive 屬性,我們可以使用介面。

ts
import { reactive } from 'vue'

interface Book {
  title: string
  year?: number
}

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

提示

不建議使用 reactive() 的泛型引數,因為返回的型別,它處理巢狀引用展開,與泛型引數型別不同。

輸入 computed()

computed() 根據獲取器的返回值推斷其型別。

ts
import { ref, computed } from 'vue'

const count = ref(0)

// inferred type: ComputedRef<number>
const double = computed(() => count.value * 2)

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

您也可以透過泛型引數指定顯式型別。

ts
const double = computed<number>(() => {
  // type error if this doesn't return a number
})

輸入事件處理程式

處理原生 DOM 事件時,正確地型別化傳遞給處理程式的引數可能很有用。讓我們看看這個例子。

vue
<script setup lang="ts">
function handleChange(event) {
  // `event` implicitly has `any` type
  console.log(event.target.value)
}
</script>

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

沒有型別註釋,event 引數將隱式具有 any 型別。如果使用 tsconfig.json 中的 "strict": true"noImplicitAny": true,這將導致 TS 錯誤。因此,建議顯式註釋事件處理程式的引數。此外,您可能需要在使用 event 的屬性時使用型別斷言。

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

輸入 Provide / Inject

提供和注入通常在單獨的元件中執行。為了正確型別化注入的值,Vue 提供了一個 InjectionKey 介面,它是一個擴充套件 Symbol 的泛型型別。它可以用來在提供者和消費者之間同步注入值的型別。

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

const key = Symbol() as InjectionKey<string>

provide(key, 'foo') // providing non-string value will result in error

const foo = inject(key) // type of foo: string | undefined

建議將注入金鑰放在單獨的檔案中,以便可以在多個元件中匯入。

當使用字串注入金鑰時,注入值的型別將是 unknown,需要透過泛型型別引數顯式宣告。

ts
const foo = inject<string>('foo') // type: string | undefined

注意注入的值仍然可以是 undefined,因為沒有保證提供者會在執行時提供此值。

可以透過提供預設值來刪除 undefined 型別。

ts
const foo = inject<string>('foo', 'bar') // type: string

如果您確定值始終被提供,也可以強制轉換值。

ts
const foo = inject('foo') as string

輸入模板引用

使用 Vue 3.5 和 @vue/language-tools 2.1(為 IDE 語言服務和 vue-tsc 提供動力),在 SFC 中由 useTemplateRef() 建立的引用的型別可以根據匹配的 ref 屬性使用的元素自動推斷靜態引用。

在無法自動推斷的情況下,您仍然可以透過泛型引數將模板引用強制轉換為顯式型別。

ts
const el = useTemplateRef<HTMLInputElement>(null)
3.5 版本之前的使用方法

模板引用應該使用顯式泛型型別引數和初始值 null 建立。

vue
<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>

要獲取正確的 DOM 介面,您可以檢查類似 MDN 的頁面。

請注意,為了確保嚴格的型別安全,在訪問 el.value 時需要使用可選鏈或型別守衛。這是因為直到元件掛載,初始引用值是 null,並且如果引用的元素透過 v-if 被解除安裝,它也可以被設定為 null

元件模板引用的型別化

從 Vue 3.5 和 @vue/language-tools 2.1(為 IDE 語言服務和 vue-tsc 提供支援)開始,SFC 中使用 useTemplateRef() 建立的引用的型別可以根據匹配的 ref 屬性所使用的元素或元件自動推斷為靜態引用。

在無法自動推斷的情況下(例如,非 SFC 使用或動態元件),您仍然可以透過泛型引數將模板引用轉換為顯式型別。

為了獲取匯入元件的例項型別,我們首先需要透過 typeof 獲取其型別,然後使用 TypeScript 的內建 InstanceType 工具提取其例項型別。

vue
<!-- App.vue -->
<script setup lang="ts">
import { useTemplateRef } from 'vue'
import Foo from './Foo.vue'
import Bar from './Bar.vue'

type FooType = InstanceType<typeof Foo>
type BarType = InstanceType<typeof Bar>

const compRef = useTemplateRef<FooType | BarType>('comp')
</script>

<template>
  <component :is="Math.random() > 0.5 ? Foo : Bar" ref="comp" />
</template>

在元件的確切型別不可用或不重要的情況下,可以使用 ComponentPublicInstance。這將只包含所有元件都共享的屬性,例如 $el

ts
import { useTemplateRef } from 'vue'
import type { ComponentPublicInstance } from 'vue'

const child = useTemplateRef<ComponentPublicInstance | null>(null)

當引用的元件是一個泛型元件時,例如 MyGenericModal

vue
<!-- MyGenericModal.vue -->
<script setup lang="ts" generic="ContentType extends string | number">
import { ref } from 'vue'

const content = ref<ContentType | null>(null)

const open = (newContent: ContentType) => (content.value = newContent)

defineExpose({
  open
})
</script>

需要使用來自 vue-component-type-helpers 庫的 ComponentExposed 來引用,因為 InstanceType 無法使用。

vue
<!-- App.vue -->
<script setup lang="ts">
import { useTemplateRef } from 'vue'
import MyGenericModal from './MyGenericModal.vue'
import type { ComponentExposed } from 'vue-component-type-helpers'

const modal = useTemplateRef<ComponentExposed<typeof MyGenericModal>>(null)

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

請注意,從 @vue/language-tools 2.1+ 開始,靜態模板引用的型別可以自動推斷,上述內容僅在邊緣情況下需要。

Composition API 的 TypeScript 已載入