使用 TypeScript 寫 Vue 組件時,有兩種推薦形式:javascript
Vue.extend()
:使用基礎 Vue 構造器,建立一個「子類」。此種寫法與 Vue 單文件組件標準形式最爲接近,惟一不一樣僅是組件選項須要被包裹在 Vue.extend()
中。vue-class-component
:一般與 vue-property-decorator
一塊兒使用,提供一系列裝飾器,能讓咱們書寫類風格的 Vue 組件。兩種形式輸出結果一致,同是建立一個 Vue 子類,但在書寫組件選項如 props,mixin 時,有些不一樣。特別是當你使用 Vue.extend()
時,爲了讓 TypeScript 正確推斷類型,你將不得不作一些額外的處理。接下來,咱們來聊一聊它們的細節差別。vue
因爲組件實例的做用域是孤立的,當從父組件傳遞數據到子組件時,咱們一般使用 Prop 選項。同時,爲了確保 Prop 的類型安全,咱們會給 Prop 添加指定類型驗證,形式以下:java
export default { props: { someProp: { type: Object, required: true, default: () => ({ message: 'test' }) } } }
咱們定義了一個 someProp,它的類型是 Object。git
使用 JavaScript 時,這並無什麼不對的地方,但當你使用 TypeScript 時,這有點不足,咱們並不能獲得有關於 someProp 更多有用的信息(好比它含有某些屬性),甚至在 TypeScript 看來,這將會是一個 any 類型:github
這意味着咱們可使用 someProp 上的任意屬性(存在或者是不存在的)均可以經過編譯。爲了防止此種狀況的發生,咱們將會給 Prop 添加類型註釋。typescript
使用 Vue.extend()
方法添加類型註釋時,須要給 type 斷言:安全
import Vue from 'vue' interface User { name: string, age: number } export default Vue.extend({ props: { testProps: { type: Object as () => User } } })
當組件內訪問 testProps 時,便能獲得相關提示:函數
然而,你必須以函數返回值的形式斷言,並不能直接斷言:ui
export default Vue.extend({ props: { testProps: { type: Object as User } } })
它會給出錯誤警告,User 接口並無實現原生 Object 構造函數所執行的方法:
Type 'ObjectConstructor' cannot be converted to type 'User'. Property 'id' is missing in type 'ObjectConstructor'.this
實際上,咱們可從 Prop type declaration :
export type Prop<T> = { (): T } | { new (...args: any[]): T & object } export type PropValidator<T> = PropOptions<T> | Prop<T> | Prop<T>[]; export interface PropOptions<T=any> { type?: Prop<T> | Prop<T>[]; required?: boolean; default?: T | null | undefined | (() => object); validator?(value: T): boolean; }
可知 Prop type 能夠以兩種不一樣方式出現:
T & object
用於下降優先級,當兩種方式同時知足時取第一種,其次它還能夠用於標記構造函數不該該返回原始類型)。當咱們指定 type 類型爲 String/Number/Boolean/Array/Object/Date/Function/Symbol 等原生構造函數時,Prop<T> 會返回它們各自簽名的返回值。
當 type 類型爲 String 構造函數時,它的調用簽名返回爲 string:
// lib.es5.d.ts interface StringConstructor { new(value?: any): String; (value?: any): string; readonly prototype: String; fromCharCode(...codes: number[]): string; }
而這也是上文中,當指定 type 類型爲 Object 構造函數時,通過 Vue 的聲明文件處理,TypeScript 推斷出爲 any 類型的緣由:
interface ObjectConstructor { new(value?: any): Object; (): any; (value: any): any; // 其它屬性 .... }
相似的,當咱們使用關鍵字 as
斷言 Object 爲 () => User
時,它能推斷出爲 User 。
從 type 第二部分可知,除傳入原生構造函數外,咱們還可傳入自定義類:
此外,這裏有個 PR 暴露一個更直觀的類型( Vue 2.6 版本才能夠用):
props: { testProp: Object as PropTypes<{ test: boolean }> }
得益於 vue-propperty-decorator Prop 修飾器,當給 Prop 增長類型推斷時,這些將變得簡單:
import { Component, Vue, Prop } from 'vue-property-decorator' @Component export default class Test extends Vue { @Prop({ type: Object }) private test: { value: string } }
當咱們在組件內訪問 test
時,便能獲取它正確的類型信息。
mixins 是一種分發 Vue 組件中可複用功能的一種方式。當在 TypeScript 中使用它時,咱們但願獲得有關於 mixins 的類型信息。
當你使用 Vue.extends()
時,這有點困難,它並不能推斷出 mixins 裏的類型:
// ExampleMixin.vue export default Vue.extend({ data () { return { testValue: 'test' } } }) // other.vue export default Vue.extend({ mixins: [ExampleMixin], created () { this.testValue // error, testValue 不存在! } })
咱們須要稍做修改:
// other.vue export default ExampleMixin.extend({ mixins: [ExampleMixin], created () { this.testValue // 編譯經過 } })
但這會存在一個問題,當使用多個 mixins 且推斷出類型時,這將沒法工做。而在這個 Issuse 中官方也明確表示,這沒法被修改。
使用 vue-class-component 這會方便不少:
// ExampleMixin.vue import Vue from 'vue' import Component from 'vue-class-component' @Component export class ExampleMixin extends Vue { public testValue = 'test' } // other.vue import Component, { mixins } from 'vue-class-component' import ExampleMixin from 'ExampleMixin.vue' @Component({ components: { ExampleMixin } }) export class MyComp extends mixins(ExampleMixin) { created () { console.log(this.testValue) // 編譯經過 } }
也支持能夠傳入多個 mixins。
作爲 Vue 中最正統的方法(與標準形式最爲接近),Vue.extends()
有着本身的優點,在 VScode Vetur 插件輔助下,它能正確提示子組件上的 Props:
而類作爲 TypeScript 特殊的存在(它既能夠做爲類型,也能夠做爲值),當咱們使用 vue-class-component
並經過 $refs
綁定爲子類組件時,便能獲取子組件上暴露的類型信息:
當你在 Vue 中使用 TypeScript 時,所遇到的第一個問題便是在 ts 文件中找不到 .vue 文件,即便你所寫的路徑並無問題:
在 TypeScript 中,它僅識別 js/ts/jsx/tsx 文件,爲了讓它識別 .vue 文件,咱們須要顯式告訴 TypeScript,vue 文件存在,而且指定導出 VueConstructor:
declare module '*.vue' { import Vue from 'vue' export default Vue }
可是,這引發了另外一個問題,當咱們導入一個並不存在的 .vue 文件時,也能經過編譯:
是的,這在情理之中。
當我嘗試在 .vue 文件中導入已存在或者不存在的 .vue 文件時,卻獲得不一樣的結果:
文件不存在時:
文件存在時:
文件不存在時,引用 Vue 的聲明文件。文件存在時,引用正確的文件定義。
這讓人很困惑,而這些都是 Vetur 的功勞。
在這個 PR 下,我找到相關解答:這個 PR 裏,Vetur 提供解析其餘 .vue 文件的功能,以便能獲取正確的信息,當 .vue 文件不存在時,會讀取 .d.ts 裏的信息。