使用組合式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>
型別引數可以是以下之一
- 一個可呼叫函式型別,但寫成具有呼叫簽名的型別字面量。它將用作返回的
emit
函式的型別。 - 一個型別字面量,其中鍵是事件名稱,值是表示事件額外接受引數的陣列/元組型別。上面的示例使用了命名元組,因此每個引數都可以有一個顯式的名稱。
正如我們所見,型別宣告使我們能夠對發射事件的型別約束有更細粒度的控制。
當不使用 <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+ 開始,靜態模板引用的型別可以自動推斷,上述內容僅在邊緣情況下需要。