Vue 2.5 & TypeScript: API 引數中的型別推導

yct21發表於2019-03-04

在剛剛釋出的 Vue.js 2.5 中加強了對 TypeScript 的支援,TypeScript 可以直接推匯出 Vue.extend(options), Vue.component(options)new Vue(options) 等 API 的引數中的 this 的型別,無需依賴 vue-class-component 這樣的 decorator。

這個功能依賴於 TypeScript 在 2.4 版本中引入的一個新特性 ThisTypeThisType 本身是一個不包含內容的 interface,其作用是人為地給定某些條件下 this 的型別。

/**
 * Marker for contextual 'this' type
 */
interface ThisType<T> { }複製程式碼

在 TypeScript 2.4 後,一個 object literal 所包含的方法的內部,其 this 的型別為:

  • 如果這個方法顯示地宣告瞭引數 this,則 this 的型別為給定引數的型別。
  • 否則,如果這個方法可以通過 contextual typing ,從方法的 signature 中得到 this 的型別,則 this 就是這個型別。
  • 否則,如果編譯選項 --noImplicitThis 開啟,並且 object literal 通過 contextual typing 得到的型別是 ThisType<T> 或者是一個包含 ThisType<T> 的 intersection,則 this 的型別為 T
  • 否則,如果編譯選項 --noImplicitThis 開啟,並且 object literal 通過 contextual typing 得到的型別不包含 ThisType<T>this 的型別為所得到的 contextual type。
  • 否則,如果編譯選項 --onImplicitThis 開啟,this 的型別為將這個方法所包含的 object literal 的型別。
  • 否則,this 的型別為 any

因此,如果一個方法通過傳入的引數來修改 this 的值(比如 Vue 將 props 中的值自動加入 Vue instance 的屬性中),可以用 ThisType<T> 來標記這個 this 的型別。

例如:

type ObjectDescriptor<D, M> = {
    data?: D;
    methods?: M & ThisType<D & M>;  // Type of 'this' in methods is D & M
}

function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
    let data: object = desc.data || {};
    let methods: object = desc.methods || {};
    return { ...data, ...methods } as D & M;
}

let obj = makeObject({
    data: { x: 0, y: 0 },
    methods: {
        moveBy(dx: number, dy: number) {
            this.x += dx;  // Strongly typed this
            this.y += dy;  // Strongly typed this
        }
    }
});

obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);複製程式碼

在這裡,makeObject 內部的 desc.methods 的型別為 M & ThisType<D & M>,因此 moveBy 方法中的 this 的型別就是 D & M,因此可以使用 this.xthis.ythis.moveBy

在 Vue 2.5 的 TypeScript 型別宣告檔案中,使用了 ThisType 的型別有

分別對應使用 array 和使用 object 作為 props 的值的 component options。

// ThisTypedComponentOptionsWithArrayProps
export default Vue.extend({
  props: ['prop1', 'prop2'],
})

// ThisTypedComponentOptionsWithRecordProps
export default Vue.extend({
  props: {
    prop1: {
      type: Number,
      default: 0,
    }
  }
})複製程式碼

使用這個型別作為引數的 API 包括:

  • function Vue (options)new Vue(options)
  • Vue.extend(options)
  • Vue.component(options)

脫離這 3 個 API,是無法使用 Vue instance 中屬性的。例如,

export default {
  props: ['prop1', 'prop2'],

  mounted() {
    console.log(this.prop1) // error TS2551: Property 'prop1' does not exist on type ...
  }
};

// 或者

const options = {
  props: ['prop1', 'prop2'],

  mounted() {
    console.log(this.prop1) // error TS2551: Property 'prop1' does not exist on type ...
  }
};

export default Vue.extend(options)複製程式碼

此外,mixin 和 global mixin 中宣告的屬性也不在這幾個 API 的引數中的方法裡所能推導的 this 型別當中。

回到上面所說的 2 個 component options 型別,

/**
 * This type should be used when an array of strings is used for a component's `props` value.
 */
export type ThisTypedComponentOptionsWithArrayProps<V extends Vue, Data, Methods, Computed, PropNames extends string> =
  object &
  ComponentOptions<V, Data | ((this: Readonly<Record<PropNames, any>> & V) => Data), Methods, Computed, PropNames[]> &
  ThisType<CombinedVueInstance<V, Data, Methods, Computed, Readonly<Record<PropNames, any>>>>;

/**
 * This type should be used when an object mapped to `PropOptions` is used for a component's `props` value.
 */
export type ThisTypedComponentOptionsWithRecordProps<V extends Vue, Data, Methods, Computed, Props> =
  object &
  ComponentOptions<V, Data | ((this: Readonly<Props> & V) => Data), Methods, Computed, RecordPropsDefinition<Props>> &
  ThisType<CombinedVueInstance<V, Data, Methods, Computed, Readonly<Props>>>;複製程式碼

這 2 個名字很長的型別其實都是使用 generic 的 type alias,內容都是 ComponentOptions 型別和一個 ThisType 型別的 intersection。在對這 2 個型別中的型別引數(包括 DataMethodsComputedPropNames 或者 Props)進行 type argument inference 的過程中,ThisType 部分實際上是作為 {} 被忽略的,options 中的 data, methods, computedprops 的型別被 ComponentOptions 捕獲。

在此之後,同一個 type alias 表示式中的 ThisType 部分也獲得了這幾個型別引數,ThisType 中使用了 CombinedVueInstance 作為型別引數,而 CombinedVueInstance 的內容為:

export type CombinedVueInstance<Instance extends Vue, Data, Methods, Computed, Props> = Instance & Data & Methods & Computed & Props;複製程式碼

實際上就是包含使用者定義的 datamethodscomputedprops 的 Vue instance,這裡面的 Props 型別引數根據傳入的引數所使用的 props 形式不同經過了又一次的轉換。

Props 的轉換過程中,如果傳入的是一個 object,則會從其中的 type 屬性的 constructor 和 default 屬性的型別推斷出這個 prop 的型別。

不過如果在 prop 的引數中不能推匯出 prop 的型別,TypeScript 會編譯錯誤,這是 Vue 2.5.2 版本的一個 bug

這樣,在之前提到的 3 個 API 的引數中,內部所包含的方法裡,this 的型別就成為了 Instance & Data & Methods & Computed & Props,我們就可以在裡面使用 Vue instance 上的屬性了。

Reference

相關文章