在 2.5.0 版本中,Vue 大大改進了型別宣告系統以更好地使用預設的基於物件的 API。
意味著當我們僅是安裝 Vue 的宣告檔案時,一切也都將會按預期進行:
this
,就是 Vue;this
屬性上,具有 Methods 選項上定義的同名函式屬性;- 在例項 data、computed、prop 上定義的屬性/方法,也都將會出現在
this
屬性上; - ......
在這篇文章裡,我們來談談上述背後的故事。
Methods
當我們建立 Vue 例項,並在 Methods 上定義方法時, this
不僅具有 Vue 例項上屬性,同時也具有與 Methods 選項上同名的函式屬性:
new Vue({
methods: {
test () {
this.$el // Vue 例項上的屬性
}
},
created () {
this.test() // methods 選項上同名的方法
this.$el // Vue 例項上的屬性
}
})
複製程式碼
為了探究其原理,我們把元件選項的宣告改寫成以下方式:
定義 Methods:
// methods 是 [key: string]: (this: Vue) => any 的集合
type Methods = Record<string, (this: Vue) => any>
複製程式碼
這會存在一個問題,Methods 上定義的方法裡的 this
,全部都是 Vue 建構函式上的方法,而不能訪問我們自定義的方法。
我們需要把 Vue 例項傳進去:
type Methods<V> = Record<string, (this: V) => any>
複製程式碼
元件選項(同樣也需要傳例項):
interface ComponentOption<V> {
methods: Methods<V>,
created?(this: V): void
}
複製程式碼
我們可以使用它:
declare function testVue<V extends Vue>(option: ComponentOption<V>): V
複製程式碼
此種情形下,我們必須將元件例項的型別顯式傳入,從而使其編譯通過:
interface TestComponent extends Vue {
test (): void
}
testVue<TestComponent>({
methods: {
test () {}
},
created () {
this.test() // 編譯通過
this.$el // 通過
}
})
複製程式碼
這有點麻煩,為了使它能按我們預期的工作,我們定義了一個額外的 interface。
在 Vue 的宣告檔案裡,使用了一種簡單的方式:通過使用 ThisType<T>
對映型別,讓 this
具有所需要的屬性。
在 TypeScript 倉庫 ThisType<T>
的 PR 下,有一個使用例子:
在這個例子中,通過對 methods 的值使用 ThisType<D & M>
,從而 TypeScript 推匯出 methods 物件中 this
即是: { x: number, y: number } & { moveBy(dx: number, dy: number ): void }
。
與此類似,我們可以讓 this
具有 Methods 上定義的同名函式屬性:
type DefaultMethods<V> = Record<string, (this: V) => any>
interface ComponentOption<
V,
Methods = DefaultMethods<V>
> {
methods: Methods,
created?(): void
}
declare function testVue<V extends Vue, Methods> (
option: ComponentOption<V, Methods> & ThisType<V & Methods>
): V & Methods
testVue({
methods: {
test () {}
},
created () {
this.test() // 編譯通過
this.$el // 例項上的屬性
}
})
複製程式碼
在上面程式碼中,我們:
- 建立了一個 ComponentOption interface,它有兩個引數,當前例項 Vue 與 預設值是
[key: string]: (this: V) => any
的 Methods。 - 定義了一個函式 testVue,同時將範型 V, Methods 傳遞給 ComponentOption 與
ThisType
。ThisType<V & Methods>
標誌著例項內的this
即是 V 與 Methods 的交叉型別。 - 當 testVue 函式被呼叫時,TypeScript 推斷出 Methods 為
{ test (): void }
,從而在例項內this
即是:Vue & { test (): void }
;
Data
得益於上文中的 ThisType<T>
,Data 的處理有點類似與 Methods,唯一不同之處 Data 可有兩種不同型別,Object 或者 Function。它的型別寫法如下:
type DefaultData<V> = object | ((this: V) => object)
複製程式碼
同樣,我們也把 ComponentOption 與 testVue 稍作修改
interface ComponentOption<
V,
Data = DefaultData<V>,
Methods = DefaultMethods<V>
> {
data: Data
methods?: Methods,
created?(): void
}
declare function testVue<V extends Vue, Data, Methods> (
option: ComponentOption<V, Data, Methods> & ThisType<V & Data & Methods>
): V & Data& Methods
複製程式碼
當 Data 是 Object 時,它能正常工作:
testVue({
data: {
testData: ''
},
created () {
this.testData // 編譯通過
}
})
複製程式碼
當我們傳入 Function 時,它並不能:
TypeScript 推斷出 Data 是 (() => { testData: string })
,這並不是期望的 { testData: string }
,我們需要對函式引數 options 的型別做少許修改,當 Data 傳入為函式時,取函式返回值:
declare function testVue<V extends Vue, Data, Method>(
option: ComponentOption<V, Data | (() => Data), Method> & ThisType<V & Data & Method>
): V & Data & Method
複製程式碼
這時候編譯可以通過:
testVue({
data () {
return {
testData: ''
}
},
created () {
this.testData // 編譯通過
}
})
複製程式碼
Computed
Computed 的處理似乎有點棘手:它與 Methods 不同,當我們在 Methods 中定義了一個方法,this
也會含有相同名字的函式屬性,而在 Computed 中定義具有返回值的方法時,我們期望 this
含有函式返回值的同名屬性。
舉個例子:
new Vue({
computed: {
testComputed () {
return ''
}
},
methods: {
testFunc () {}
},
created () {
this.testFunc() // testFunc 是一個函式
this.testComputed // testComputed 是 string,並不是一個返回值為 string 的函式
}
})
複製程式碼
我們需要一個對映型別,把定義在 Computed 內具有返回值的函式,對映為 key 為函式名,值為函式返回值的新型別:
type Accessors<T> = {
[K in keyof T]: (() => T[K])
}
複製程式碼
Accessors<T>
將會把型別 T,對映為具有相同屬性名稱,值為函式返回值的新型別,在型別推斷時,此過程相反。
接著,我們補充上例:
// Computed 是一組 [key: string]: any 的集合
type DefaultComputed = Record<string, any>
interface ComponentOption<
V,
Data = DefaultData<V>,
Computed = DefaultComputed,
Methods = DefaultMethods<V>
> {
data?: Data,
computed?: Accessors<Computed>
methods?: Methods,
created?(): void
}
declare function testVue<V extends Vue, Data, Compted, Methods> (
option: ComponentOption<V, Data | (() => Data), Compted, Methods> & ThisType<V & Data & Compted & Methods>
): V & Data & Compted & Methods
testVue({
computed: {
testComputed () {
return ''
}
},
created () {
this.testComputed // string
}
})
複製程式碼
當呼叫 testVue 時,我們傳入一個屬性為 testComputed () => ''
的 Computed,TypeScript 會嘗試將型別對映至 Accessors<T>
,從而推匯出 Computed 即是 { testComputed: string }
。
此外,Computed 具有另一個寫法:get 與 set 形式,我們只需要把對映型別做相應補充即可:
interface ComputedOptions<T> {
get?(): T,
set?(value: T): void
}
type Accessors<T> = {
[K in keyof T]: (() => T[K]) | ComputedOptions<T[K]>
}
複製程式碼
Prop
在上篇文章在 Vue 中使用 TypeScript 的一些思考(實踐)中,我們已經討論了 Prop 的推導,在此不再贅述。
最後
此篇文章是對 Vue typings 的一次簡單解讀,希望大家看得懂原始碼時,不要忘記了 Vue typings,畢竟 Vue typings 才是給程式行為以提示和約束的關鍵。
參考
- https://github.com/Microsoft/TypeScript/pull/14141
- http://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#mapped-types
- https://github.com/vuejs/vue/blob/dev/types/options.d.ts