详解在Vue中使用TypeScript的一些思考(实践)


Posted in Javascript onJuly 06, 2018

Vue.extend or vue-class-component

使用 TypeScript 写 Vue 组件时,有两种推荐形式:

  • Vue.extend():使用基础 Vue 构造器,创建一个“子类”。此种写法与 Vue 单文件组件标准形式最为接近,唯一不同仅是组件选项需要被包裹在 Vue.extend() 中。
  • vue-class-component:通常与 vue-property-decorator 一起使用,提供一系列装饰器,能让我们书写类风格的 Vue 组件。

两种形式输出结果一致,同是创建一个 Vue 子类,但在书写组件选项如 props,mixin 时,有些不同。特别是当你使用 Vue.extend() 时,为了让 TypeScript 正确推断类型,你将不得不做一些额外的处理。接下来,我们来聊一聊它们的细节差异。

Prop

由于组件实例的作用域是孤立的,当从父组件传递数据到子组件时,我们通常使用 Prop 选项。同时,为了确保 Prop 的类型安全,我们会给 Prop 添加指定类型验证,形式如下:

export default {
 props: {
  someProp: {
   type: Object,
   required: true,
   default: () => ({ message: 'test' })
  }
 }
}

我们定义了一个 someProp,它的类型是 Object。

使用 JavaScript 时,这并没有什么不对的地方,但当你使用 TypeScript 时,这有点不足,我们并不能得到有关于 someProp 更多有用的信息(比如它含有某些属性),甚至在 TypeScript 看来,这将会是一个 any 类型:

详解在Vue中使用TypeScript的一些思考(实践)

这意味着我们可以使用 someProp 上的任意属性(存在或者是不存在的)都可以通过编译。为了防止此种情况的发生,我们将会给 Prop 添加类型注释。

Vue.extend()

使用 Vue.extend() 方法添加类型注释时,需要给 type 断言:

import Vue from 'vue'

interface User {
 name: string,
 age: number
}

export default Vue.extend({
 props: {
  testProps: {
   type: Object as () => User
  }
 }
})

当组件内访问 testProps 时,便能得到相关提示:

详解在Vue中使用TypeScript的一些思考(实践)

然而,你必须以函数返回值的形式断言,并不能直接断言:

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'.

实际上,我们可从 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 可以以两种不同方式出现:

  • 含有一个调用签名的范型 type,该签名返回 T;
  • 一个范型构造函数签名,该函数创建指定类型 T 对象 (返回值 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 第二部分可知,除传入原生构造函数外,我们还可传入自定义类:

详解在Vue中使用TypeScript的一些思考(实践)

此外,这里有个 PR 暴露一个更直观的类型( Vue 2.6 版本才可以用):

props: {
 testProp: Object as PropTypes<{ test: boolean }>
}

vue-class-component

得益于 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

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:

详解在Vue中使用TypeScript的一些思考(实践)

而类做为 TypeScript 特殊的存在(它既可以作为类型,也可以作为值),当我们使用 vue-class-component 并通过 $refs 绑定为子类组件时,便能获取子组件上暴露的类型信息:

详解在Vue中使用TypeScript的一些思考(实践)

导入 .vue 时,为什么会报错?

当你在 Vue 中使用 TypeScript 时,所遇到的第一个问题即是在 ts 文件中找不到 .vue 文件,即使你所写的路径并没有问题:

详解在Vue中使用TypeScript的一些思考(实践)

在 TypeScript 中,它仅识别 js/ts/jsx/tsx 文件,为了让它识别 .vue 文件,我们需要显式告诉 TypeScript,vue 文件存在,并且指定导出 VueConstructor:

declare module '*.vue' {
 import Vue from 'vue'
 export default Vue
}

但是,这引起了另一个问题,当我们导入一个并不存在的 .vue 文件时,也能通过编译:

详解在Vue中使用TypeScript的一些思考(实践)

是的,这在情理之中。

当我尝试在 .vue 文件中导入已存在或者不存在的 .vue 文件时,却得到不同的结果:

文件不存在时:

详解在Vue中使用TypeScript的一些思考(实践)

文件存在时:

详解在Vue中使用TypeScript的一些思考(实践)

文件不存在时,引用 Vue 的声明文件。文件存在时,引用正确的文件定义。

这让人很困惑,而这些都是 Vetur 的功劳。

在这个 PR 下,我找到相关解答:这个 PR 里,Vetur 提供解析其他 .vue 文件的功能,以便能获取正确的信息,当 .vue 文件不存在时,会读取 .d.ts 里的信息。

参考

https://github.com/vuejs/vue/pull/5887

https://github.com/vuejs/vue/issues/7211

https://github.com/vuejs/vue/pull/6856

https://github.com/vuejs/vue/pull/5887/files/1092efe6070da2052a8df97a802c9434436eef1e#diff-23d7799dcc9e9be419d28a15348b0d99

https://github.com/Microsoft/TypeScript/blob/8e47c18636da814117071a2640ccf87c5f16fcfd/src/compiler/types.ts#L3563-L3583

https://github.com/vuejs/vetur/pull/94

Javascript 相关文章推荐
在chrome浏览器中,防止input[text]和textarea在聚焦时出现黄色边框的解决方法
May 24 Javascript
js String对象中常用方法小结(字符串操作)
Jan 27 Javascript
document.createElement()用法
Mar 13 Javascript
jquery.Ajax()方法调用Asp.Net后台的方法解析
Feb 13 Javascript
javascript内置对象操作详解
Feb 04 Javascript
深入理解JavaScript系列(25):设计模式之单例模式详解
Mar 03 Javascript
jQuery移动页面开发中的触摸事件与虚拟鼠标事件简介
Dec 03 Javascript
JavaScript中const、var和let区别浅析
Oct 11 Javascript
基于JavaScript实现屏幕滚动效果
Jan 18 Javascript
JavaScript实现弹窗效果代码分析
Mar 09 Javascript
关于vue.js v-bind 的一些理解和思考
Jun 06 Javascript
前端跨域的几种解决方式总结(推荐)
Aug 16 Javascript
javascript显示动态时间的方法汇总
Jul 06 #Javascript
Django+Vue跨域环境配置详解
Jul 06 #Javascript
微信小程序画布圆形进度条显示效果
Nov 17 #Javascript
微信小程序实时聊天WebSocket
Jul 05 #Javascript
vue v-model实现自定义样式多选与单选功能
Jul 05 #Javascript
基于vue展开收起动画的示例代码
Jul 05 #Javascript
微信小程序实现星级评分和展示
Jul 05 #Javascript
You might like
ThinkPHP公共配置文件与各自项目中配置文件组合的方法
2014/11/24 PHP
php简单实现无限分类树形列表的方法
2015/03/27 PHP
PHP+mysql实现从数据库获取下拉树功能示例
2017/01/06 PHP
Ajax,UTF-8还是GB2312 eval 还是execScript
2008/11/13 Javascript
javascript concat数组累加 示例
2009/09/03 Javascript
jquery 表格分页等操作实现代码(pagedown,pageup)
2010/04/11 Javascript
JS实现侧悬浮浮动实例代码
2013/11/29 Javascript
js 触发select onchange事件代码
2014/03/20 Javascript
js中substring和substr的定义和用法
2014/05/05 Javascript
jQuery中:reset选择器用法实例
2015/01/04 Javascript
javasript实现密码的隐藏与显示
2015/05/08 Javascript
初识Javascript小结
2015/07/16 Javascript
Bootstrap树形组件jqTree的简单封装
2016/01/25 Javascript
JS实现上下左右对称的九九乘法表
2016/02/22 Javascript
jQuery对象的链式操作用法分析
2016/05/10 Javascript
jQuery组件easyui基本布局实现代码
2016/08/25 Javascript
Angular 应用技巧总结
2016/09/14 Javascript
JavaScript比较两个数组的内容是否相同(推荐)
2017/05/02 Javascript
详解vuejs几种不同组件(页面)间传值的方式
2017/06/01 Javascript
vue-router路由懒加载和权限控制详解
2017/12/13 Javascript
详解js中let与var声明变量的区别
2020/04/05 Javascript
如何实现双向绑定mvvm的原理实现
2019/05/28 Javascript
微信小程序 高德地图路线规划实现过程详解
2019/08/05 Javascript
详解ECMAScript2019/ES10新属性
2019/12/06 Javascript
python获取android设备的GPS信息脚本分享
2015/03/06 Python
Python新手们容易犯的几个错误总结
2017/04/01 Python
python 3.5实现检测路由器流量并写入txt的方法实例
2017/12/17 Python
python方法生成txt标签文件的实例代码
2018/05/10 Python
解决Tensorboard可视化错误:不显示数据 No scalar data was found
2020/02/15 Python
Matplotlib使用字符串代替变量绘制散点图的方法
2020/02/17 Python
HTML5使用ApplicationCache接口实现离线缓存技术解决离线难题
2012/12/13 HTML / CSS
定制别致的瑜伽垫:Sugarmat
2019/06/21 全球购物
工厂门卫岗位职责
2013/11/25 职场文书
推广普通话共筑中国梦演讲稿
2014/09/21 职场文书
用Python的绘图库(matplotlib)绘制小波能量谱
2021/04/17 Python
python自动计算图像数据集的RGB均值
2021/06/18 Python