2024-11-03|閱讀時間 ‧ 約 0 分鐘

EP57 - 選項 API 中使用 TypeScript

TypeScript with Options API, 上一篇組件API已經蠻頭大了
選項API雖然是舊版Vue的方法,但還是對整個框架的原理有幫助吧~
繼續來看看吧~

這篇文章假設您已經閱讀了《使用 TypeScript 的 Vue 概述》。

提示:雖然 Vue 支援使用 Options API 的 TypeScript,但建議透過 Composition API 使用 TypeScript,因為這樣可以提供更簡單、更高效和更穩健的類型推斷。

元件屬性類型標註 - Typing Component Props

在 Options API 中,對屬性的類型推斷需要使用 defineComponent() 將元件包裝起來。這樣一來,Vue 就能根據屬性選項推斷屬性的類型,並考慮其他選項,例如 required: truedefault

import { defineComponent } from 'vue'

export default defineComponent({
// 啟用類型推斷
props: {
name: String,
id: [Number, String],
msg: { type: String, required: true },
metadata: null
},
mounted() {
this.name // 類型:string | undefined
this.id // 類型:number | string | undefined
this.msg // 類型:string
this.metadata // 類型:any
}
})

然而,運行時的屬性選項僅支持使用構造函數作為屬性類型,無法指定複雜類型,例如具有嵌套屬性的物件或函數調用簽名。

要標註複雜的屬性類型,我們可以使用 PropType 實用類型:

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

interface Book {
title: string
author: string
year: number
}

export default defineComponent({
props: {
book: {
// 提供更具體的 `Object` 類型
type: Object as PropType<Book>,
required: true
},
// 也可以標註函數
callback: Function as PropType<(id: number) => void>
},
mounted() {
this.book.title // string
this.book.year // number

// TS 錯誤:類型 'string' 的參數
// 無法分配給類型 'number' 的參數
this.callback?.('123')
}
})

注意事項

如果您的 TypeScript 版本低於 4.7,當使用函數值作為驗證器和預設屬性選項時,必須小心,確保使用箭頭函數:

ts
複製程式碼import { defineComponent } from 'vue'
import type { PropType } from 'vue'

interface Book {
title: string
year?: number
}

export default defineComponent({
props: {
bookA: {
type: Object as PropType<Book>,
// 如果您的 TypeScript 版本低於 4.7,請確保使用箭頭函數
default: () => ({
title: '箭頭函數表達式'
}),
validator: (book: Book) => !!book.title
}
}
})

這樣可以防止 TypeScript 在這些函數內部推斷 this 的類型,這不幸地會導致類型推斷失敗。這是之前的設計限制,現在在 TypeScript 4.7 中已經有所改善。

元件事件類型標註 - Typing Component Emits

我們可以使用 emits 選項的物件語法來宣告發射事件的預期有效載荷類型。此外,所有未聲明的發射事件在被呼叫時都會拋出類型錯誤:

import { defineComponent } from 'vue'

export default defineComponent({
emits: {
addBook(payload: { bookName: string }) {
// 執行運行時驗證
return payload.bookName.length > 0
}
},
methods: {
onSubmit() {
this.$emit('addBook', {
bookName: 123 // 類型錯誤!
})

this.$emit('non-declared-event') // 類型錯誤!
}
}
})

元件計算屬性類型標註 - Typing Computed Properties

計算屬性會根據其返回值推斷其類型:

import { defineComponent } from 'vue'

export default defineComponent({
data() {
return {
message: 'Hello!'
}
},
computed: {
greeting() {
return this.message + '!'
}
},
mounted() {
this.greeting // 類型: string
}
})

在某些情況下,您可能希望明確標註計算屬性的類型,以確保其實現是正確的:

import { defineComponent } from 'vue'

export default defineComponent({
data() {
return {
message: 'Hello!'
}
},
computed: {
// 明確標註返回類型
greeting(): string {
return this.message + '!'
},

// 標註可寫的計算屬性
greetingUppercased: {
get(): string {
return this.greeting.toUpperCase()
},
set(newValue: string) {
this.message = newValue.toUpperCase()
}
}
}
})

在某些邊緣情況下,當 TypeScript 因循環推斷循環而無法推斷計算屬性的類型時,也可能需要明確的標註。

事件處理程序類型標註 - Typing Event Handlers

在處理原生 DOM 事件時,正確標註傳遞給處理程序的參數類型可能會很有用。讓我們看一下這個例子:

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
methods: {
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 錯誤。因此,建議明確標註事件處理程序的參數。此外,當訪問事件的屬性時,您可能需要使用類型斷言:

import { defineComponent } from 'vue'

export default defineComponent({
methods: {
handleChange(event: Event) {
console.log((event.target as HTMLInputElement).value)
}
}
})

擴充全域屬性 - Augmenting Global Properties

一些插件通過 app.config.globalProperties 為所有組件實例安裝全域可用的屬性。例如,我們可以安裝 this.$http 用於數據獲取或 this.$translate 用於國際化。為了讓這與 TypeScript 相容,Vue 提供了 ComponentCustomProperties 接口,旨在通過 TypeScript 模組擴充來增強:

import axios from 'axios'

declare module 'vue' {
interface ComponentCustomProperties {
$http: typeof axios
$translate: (key: string) => string
}
}

參考:

類型擴充位置 - Type Augmentation Placement

我們可以將這種類型擴充放在 .ts 檔案中,或者放在全專案的 *.d.ts 檔案中。無論哪種方式,請確保它包含在 tsconfig.json 中。對於庫或插件作者,這個檔案應在 package.jsontypes 屬性中指定。

為了利用模組擴充,您需要確保擴充放置在 TypeScript 模組中。也就是說,該檔案需要包含至少一個頂層的 import 或 export,即使它只是 export {}。如果擴充放置在模組之外,它將覆蓋原始類型,而不是進行增強!

// 不工作,覆蓋原始類型。
declare module 'vue' {
interface ComponentCustomProperties {
$translate: (key: string) => string
}
}
// 正常工作
export {}

declare module 'vue' {
interface ComponentCustomProperties {
$translate: (key: string) => string
}
}

擴充自訂選項 - Augmenting Custom Options

一些插件,例如 vue-router,提供對自訂組件選項的支持,例如 beforeRouteEnter

import { defineComponent } from 'vue'

export default defineComponent({
beforeRouteEnter(to, from, next) {
// ...
}
})

如果沒有適當的類型擴充,這個鉤子的參數將隱式具有 any 類型。我們可以擴充 ComponentCustomOptions 接口來支持這些自訂選項:

import { Route } from 'vue-router'

declare module 'vue' {
interface ComponentCustomOptions {
beforeRouteEnter?(to: Route, from: Route, next: () => void): void
}
}

現在 beforeRouteEnter 選項將被正確類型化。請注意,這只是一個示例,像 vue-router 這樣的良好類型化庫應該會自動在其自己的類型定義中執行這些擴充。

這種擴充的放置遵循與全域屬性擴充相同的限制

參考:

Q: 所以組件api跟項目api在typescript差異?

在 TypeScript 中,組件 API(Component API)和項目 API(Project API)的差異主要在於它們的使用場景和型別定義方式。以下是一些關鍵的差異:

組件 API

  1. 定義範圍
    • 組件 API 是針對單個組件的接口,通常包括該組件的 props、emits、computed 屬性和方法。
  2. 型別推斷
    • 使用 defineComponent() 時,TypeScript 能夠自動推斷組件的屬性類型。例如,對於 props 的定義,TypeScript 能夠從 props 選項中推斷型別。
  3. 使用的主要範例
    • 在組件內部定義 props、emits、methods 和 lifecycle hooks 的型別化。

項目 API

  1. 定義範圍
    • 項目 API 是針對整個項目的接口,通常涉及到全局屬性、插件和整個應用程序的配置。
  2. 型別增強
    • 使用 TypeScript 的模組擴充(Module Augmentation)來增強 Vue 的全局屬性,如 app.config.globalProperties,以使其能與 TypeScript 進行協同工作。
  3. 使用的主要範例
    • 對全局屬性進行型別定義,例如添加 $http$translate 屬性,並使用 declare module 來增強 Vue 的內部類型。

總結

  • 組件 API 更加專注於組件的內部結構和類型推斷,適用於單個組件的開發;而 項目 API 則是針對整個應用程序的配置和全局屬性,通常需要透過模組擴充來進行型別定義。
  • 組件 API 通常使用 defineComponent(),而項目 API 通常需要使用 TypeScript 的模組擴充來增強 Vue 的全局類型。這使得開發者能夠在整個應用中使用這些自定義的全局屬性和方法。
快來洗洗睡覺吧~
多接觸TypeScript好像慢慢理解~
他就是對各種東西都給他Type!的Script啊www
分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.