从 Vue typings 看 “this”

在 2.5.0 版本中,Vue 大大改进了类型声明系统以更好地使用默认的基于对象的 API。html

意味着当咱们仅是安装 Vue 的声明文件时,一切也都将会按预期进行:vue

  • this,就是 Vue;
  • this 属性上,具备 Methods 选项上定义的同名函数属性;
  • 在实例 data、computed、prop 上定义的属性/方法,也都将会出如今 this 属性上;
  • ......

在这篇文章里,咱们来谈谈上述背后的故事。git

Methods

当咱们建立 Vue 实例,并在 Methods 上定义方法时, this 不只具备 Vue 实例上属性,同时也具备与 Methods 选项上同名的函数属性:github

new Vue({
  methods: {
    test () {
     this.$el   // Vue 实例上的属性
    }
  },
  
  created () {
    this.test() // methods 选项上同名的方法
    this.$el    // Vue 实例上的属性
  }
})
复制代码

为了探究其原理,咱们把组件选项的声明改写成如下方式:typescript

定义 Methods:app

// 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>
复制代码

组件选项(一样也须要传实例):ui

interface ComponentOption<V> {
  methods: Methods<V>,
  created?(this: V): void
}
复制代码

咱们可使用它:this

declare function testVue<V extends Vue>(option: ComponentOption<V>): V 复制代码

此种情形下,咱们必须将组件实例的类型显式传入,从而使其编译经过:spa

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 与 ThisTypeThisType<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
相关文章
相关标签/搜索