TypeScript with Composition API 老實說還是不太清楚好處
不過我知道每次開發的時候型別的錯誤,常常需要debug
組件API下的TypeScript有什麼特別嗎?
假設你已經閱讀過上篇有關使用 TypeScript 開發 Vue 的內容。
<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() 的參數將用作運行時屬性選項。
然而,通常更直接的方式是通過泛型類型參數定義純類型的屬性:
<script setup lang="ts">
const props = defineProps<{
foo: string
bar?: number
}>()
</script>
這稱為「基於類型的聲明」。編譯器會盡其所能根據類型參數推斷等效的運行時選項。在這種情況下,我們的第二個例子將編譯為與第一個例子完全相同的運行時選項。
您可以使用基於類型的聲明或運行時聲明,但不能同時使用兩者。
我們還可以將屬性類型移到單獨的接口中:
<script setup lang="ts">
interface Props {
foo: string
bar?: number
}
const props = defineProps<Props>()
</script>
如果 Props 是從外部來源導入的,這同樣有效。這個功能需要 TypeScript 作為 Vue 的對等依賴項。
<script setup lang="ts">
import type { Props } from './foo'
const props = defineProps<Props>()
</script>
在 3.2 版本及以下,defineProps() 的泛型類型參數僅限於類型文字或本地接口的引用。
這個限制在 3.3 版本中已被解決。最新版本的 Vue 支持在類型參數位置引用導入的和一組有限的複雜類型。但是,由於類型到運行時轉換仍然是基於 AST 的,一些需要實際類型分析的複雜類型(例如條件類型)是不支持的。您可以對單個屬性的類型使用條件類型,但不能對整個屬性對象使用。
使用基於類型的聲明時,我們失去了為屬性聲明默認值的能力。這可以通過使用反應式屬性解構來解決:
interface Props {
msg?: string
labels?: string[]
}
const { msg = 'hello', labels = ['one', 'two'] } = defineProps<Props>()
在 3.4 及以下版本中,反應式屬性解構不是默認啟用的。一個替代方案是使用 withDefaults 編譯器巨集:
interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
labels: () => ['one', 'two']
})
這將編譯為等效的運行時屬性默認選項。此外,withDefaults 幫助器提供了默認值的類型檢查,並確保返回的屬性類型對於聲明了默認值的屬性移除了可選標誌。
注意 當使用 withDefaults 時,可變引用類型(如數組或對象)的默認值應該包裝在函數中,以避免意外修改和外部副作用。這確保每個組件實例獲得默認值的自己的副本。使用解構的默認值時不需要這樣做。
<script setup>
如果不使用 <script setup>,則需要使用 defineComponent() 來啟用屬性類型推斷。傳遞給 setup() 的屬性對象的類型是從屬性選項推斷的。
import { defineComponent } from 'vue'
export default defineComponent({
props: {
message: String
},
setup(props) {
props.message // <-- type: string
}
})
使用基於類型的聲明時,屬性可以像任何其他類型一樣使用複雜類型:
<script setup lang="ts">
interface Book {
title: string
author: string
year: number
}
const props = defineProps<{
book: Book
}>()
</script>
對於運行時聲明,我們可以使用 PropType 實用類型:
import type { PropType } from 'vue'
const props = defineProps({
book: Object as PropType<Book>
})
如果我們直接指定屬性選項,這也以相同的方式工作:
import { defineComponent } from 'vue'
import type { PropType } from 'vue'
export default defineComponent({
props: {
book: Object as PropType<Book>
}
})
屬性選項更常用於選項 API,因此您會在 TypeScript 與選項 API 的指南中找到更多詳細的示例。這些示例中顯示的技術也適用於使用 defineProps() 的運行時聲明。
在 <script setup>
中,emit 函數也可以使用運行時聲明或類型聲明來進行類型化:
<script setup lang="ts">
// 運行時
const emit = defineEmits(['change', 'update'])
// 選項方式
const emit = defineEmits({
change: (id: number) => {
// 返回 `true` 或 `false` 以指示驗證通過 / 失敗
},
update: (value: string) => {
// 返回 `true` 或 `false` 以指示驗證通過 / 失敗
}
})
// 類型聲明
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
// 3.3+: 另一種更簡潔的語法
const emit = defineEmits<{
change: [id: number]
update: [value: string]
}>()
</script>
類型參數可以是以下之一:
我們可以看到,類型聲明讓我們對發出的事件的類型約束有了更精細的控制。
當不使用 <script setup>
時,defineComponent()
可以推斷 setup 上下文中暴露的 emit 函數允許的事件:
import { defineComponent } from 'vue'
export default defineComponent({
emits: ['change'],
setup(props, { emit }) {
emit('change') // <-- 類型檢查 / 自動完成
}
})
ref()
型別化 - Typing ref()ref
會從初始值推斷類型:
import { ref } from 'vue'
// 推斷類型:Ref<number>
const year = ref(2020)
// => TS 錯誤:類型 'string' 不能分配給類型 '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 // ok!
或者,在調用 ref()
時傳遞泛型參數以覆蓋默認推斷:
// 結果類型:Ref<string | number>
const year = ref<string | number>('2020')
year.value = 2020 // ok!
如果指定了泛型參數但省略了初始值,則結果類型將是包含 undefined 的聯合類型:
// 推斷類型:Ref<number | undefined>
const n = ref<number>()
reactive()
型別化 - Typing reactive()reactive()
也會從其參數隱式推斷類型:
import { reactive } from 'vue'
// 推斷類型:{ title: string }
const book = reactive({ title: 'Vue 3 Guide' })
要顯式類型化一個響應式屬性,我們可以使用接口:
import { reactive } from 'vue'
interface Book {
title: string
year?: number
}
const book: Book = reactive({ title: 'Vue 3 Guide' })
提示:不建議使用reactive()
的泛型參數,因為返回的類型(處理嵌套的ref
解包)與泛型參數類型不同。
computed()
型別化 - Typing computed()computed()
根據 getter 的返回值推斷其類型:
import { ref, computed } from 'vue'
const count = ref(0)
// 推斷類型:ComputedRef<number>
const double = computed(() => count.value * 2)
// => TS 錯誤:屬性 'split' 不存在於類型 'number' 上
const result = double.value.split('')
你也可以通過泛型參數指定顯式類型:
const double = computed<number>(() => {
// 類型錯誤如果這個不返回一個 number
})
在處理原生 DOM 事件時,將我們傳遞給處理程序的參數正確類型化可能會很有用。讓我們來看看這個例子:
<script setup lang="ts">
function handleChange(event) {
// `event` 默認為 `any` 類型
console.log(event.target.value)
}
</script>
<template>
<input type="text" @change="handleChange" />
</template>
如果沒有類型註釋,事件參數將隱式地具有 any
類型。如果在 tsconfig.json
中使用了 "strict": true
或 "noImplicitAny": true
,這也會導致 TS 錯誤。因此,建議顯式註明事件處理程序的參數類型。此外,在訪問事件的屬性時,可能需要使用類型斷言:
function handleChange(event: Event) {
console.log((event.target as HTMLInputElement).value)
}
提供和注入通常是在不同的組件中進行的。為了正確類型化注入的值,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
建議將注入鍵放在一個單獨的文件中,以便可以在多個組件中導入。
當使用字符串注入鍵時,注入值的類型將是未知的,需要通過泛型參數顯式聲明:
const foo = inject<string>('foo') // 類型:string | undefined
注意,注入的值仍然可以是 undefined,因為在運行時無法保證提供者會提供此值。
可以通過提供默認值來移除 undefined 類型:
const foo = inject<string>('foo', 'bar') // 類型:string
如果你確信該值始終會被提供,也可以強制轉換該值:
const foo = inject('foo') as string
從 Vue 3.5 和 @vue/language-tools
2.1 開始(為 IDE 語言服務和 vue-tsc
提供支持),SFC 中通過 useTemplateRef()
創建的引用類型可以根據 ref 屬性使用的元素自動推斷靜態引用的類型。
在無法自動推斷的情況下,仍然可以通過泛型參數將模板引用顯式類型化:
const el = useTemplateRef<HTMLInputElement>(null)
模板引用應該使用明確的泛型參數和初始值 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>
為了獲得正確的 DOM 接口,可以查看 MDN 等頁面。
注意,為了嚴格的類型安全,在訪問 el.value
時需要使用可選鏈接或類型守衛。這是因為初始 ref 值為 null,直到組件被掛載,而且如果引用的元素被 v-if
卸載,也可能會設置為 null。
在 Vue 3.5 和 @vue/language-tools 2.1 中(為 IDE 語言服務和 vue-tsc 提供支持),在單文件組件 (SFC) 中使用 useTemplateRef()
創建的引用類型,可以根據匹配的 ref 屬性使用的元素或組件自動推斷。
在無法自動推斷的情況下(例如非 SFC 使用或動態組件),您仍然可以通過泛型參數將模板引用強制轉換為明確的類型。
為了獲取導入組件的實例類型,我們需要首先通過 typeof
獲取其類型,然後使用 TypeScript 的內建 InstanceType
實用工具來提取其實例類型:
<!-- 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
:
import { useTemplateRef } from 'vue'
import type { ComponentPublicInstance } from 'vue'
const child = useTemplateRef<ComponentPublicInstance | null>(null)
在引用的組件是泛型組件的情況下,例如 MyGenericModal
:
<!-- 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
將不起作用。
<!-- 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+,靜態模板引用的類型可以自動推斷,以上情況僅在特殊情況下需要。
感覺TypeScript還很多要學習啊的www
對這個還不太熟~但好處蠻多~