Vue 2.5 & TypeScript: API 参数中的类型推导

3,455 阅读4分钟

在刚刚发布的 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