直接跳到內容

TypeScript 與選項式 API

這一章假設你已經閱讀了搭配 TypeScript 使用 Vue 的概覽。

TIP

雖然 Vue 的確支持在選項式 API 中使用 TypeScript,但在使用 TypeScript 的前提下更推薦使用組合式 API,因為它提供了更簡單、高效和可靠的類型推導。

為組件的 props 標註類型

選項式 API 中對 props 的類型推導需要用 defineComponent() 來包裝組件。有了它,Vue 才可以通過 props 以及一些額外的選項,例如 required: truedefault 來推導出 props 的類型:

ts
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
  }
})

然而,這種運行時 props 選項僅支持使用構造函數來作為一個 prop 的類型——沒有辦法指定多層級對象或函數簽名之類的複雜類型。

我們可以使用 PropType 這個工具類型來標記更復雜的 props 類型:

ts
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 Error: argument of type 'string' is not
    // assignable to parameter of type 'number'
    this.callback?.('123')
  }
})

注意事項

如果你的 TypeScript 版本低於 4.7,在使用函數作為 prop 的 validatordefault 選項值時需要格外小心——確保使用箭頭函數:

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: 'Arrow Function Expression'
      }),
      validator: (book: Book) => !!book.title
    }
  }
})

這會防止 TypeScript 將 this 根據函數內的環境作出不符合我們期望的類型推導。這是之前版本的一個設計限制,不過現在已經在 TypeScript 4.7 中解決了。

為組件的 emits 標註類型

我們可以給 emits 選項提供一個對象來聲明組件所觸發的事件,以及這些事件所期望的參數類型。試圖觸發未聲明的事件會拋出一個類型錯誤:

ts
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') // 類型錯誤
    }
  }
})

為計算屬性標記類型

計算屬性會自動根據其返回值來推導其類型:

ts
import { defineComponent } from 'vue'

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

在某些場景中,你可能想要顯式地標記出計算屬性的類型以確保其實現是正確的:

ts
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 因循環引用而無法推導類型的情況下,可能必須進行顯式的類型標註。

為事件處理函數標註類型

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

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

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

ts
import { defineComponent } from 'vue'

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

擴展全局屬性

某些插件會通過 app.config.globalProperties 為所有組件都安裝全局可用的屬性。舉例來說,我們可能為了請求數據而安裝了 this.$http,或者為了國際化而安裝了 this.$translate。為了使 TypeScript 更好地支持這個行為,Vue 暴露了一個被設計為可以通過 TypeScript 模塊擴展來擴展的 ComponentCustomProperties 接口:

ts
import axios from 'axios'

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

參考:

類型擴展的位置

我們可以將這些類型擴展放在一個 .ts 文件,或是一個影響整個項目的 *.d.ts 文件中。無論哪一種,都應確保在 tsconfig.json 中包括了此文件。對於庫或插件作者,這個文件應該在 package.jsontypes 屬性中被列出。

為了利用模塊擴展的優勢,你需要確保將擴展的模塊放在 TypeScript 模塊 中。 也就是說,該文件需要包含至少一個頂級的 importexport,即使它只是 export {}。如果擴展被放在模塊之外,它將覆蓋原始類型,而不是擴展!

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

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

擴展自定義選項

某些插件,例如 vue-router,提供了一些自定義的組件選項,例如 beforeRouteEnter

ts
import { defineComponent } from 'vue'

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

如果沒有確切的類型標註,這個鉤子函數的參數會隱式地標註為 any 類型。我們可以為 ComponentCustomOptions 接口擴展自定義的選項來支持:

ts
import { Route } from 'vue-router'

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

現在這個 beforeRouteEnter 選項會被準確地標註類型。注意這只是一個例子——像 vue-router 這種類型完備的庫應該在它們自己的類型定義中自動執行這些擴展。

這種類型擴展和全局屬性擴展受到相同的限制

參考:

TypeScript 與選項式 API已經加載完畢