详解在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 相关文章推荐
JavaScript 面向对象的 私有成员和公开成员
May 13 Javascript
基于jquery的一行代码轻松实现拖动效果
Dec 28 Javascript
jquery简单实现外部链接用新窗口打开的方法
May 30 Javascript
原生态js,鼠标按下后,经过了那些单元格的简单实例
Aug 11 Javascript
Asp.Net之JS生成分页条的方法
Nov 23 Javascript
详解JavaScript中的属性和特性
Dec 08 Javascript
整理一些最近经常遇到的前端面试题
Apr 25 Javascript
使用vue构建移动应用实战代码
Aug 02 Javascript
vue使用监听实现全选反选功能
Jul 06 Javascript
vue登录注册实例详解
Sep 14 Javascript
关于vue路由缓存清除在main.js中的设置
Nov 06 Javascript
js实现随机抽奖
Mar 19 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
PHP 5昨天隆重推出--PHP 5/Zend Engine 2.0新特性
2006/10/09 PHP
用phpmyadmin更改mysql5.0登录密码
2008/03/25 PHP
PHP CodeBase:将时间显示为&quot;刚刚&quot;&quot;n分钟/小时前&quot;的方法详解
2013/06/06 PHP
探讨PHP调用时间格式的参数详解
2013/06/06 PHP
PHP实现对png图像进行缩放的方法(支持透明背景)
2015/07/15 PHP
PHP人民币金额转大写实例代码
2015/10/02 PHP
extjs fckeditor集成代码
2009/05/10 Javascript
jQuery的.live()和.die() 使用介绍
2011/09/10 Javascript
javascript表单验证 - Parsley.js使用和配置
2013/01/25 Javascript
下拉菜单点击实现连接跳转功能的js代码
2013/05/19 Javascript
Jquery 模拟用户点击超链接或者按钮的方法
2013/10/25 Javascript
JS关闭窗口与JS关闭页面的几种方法小结
2013/12/17 Javascript
浅析Cookie中的Path与domain
2013/12/18 Javascript
Javascript中的默认参数详解
2014/10/22 Javascript
JavaScript的this关键字的理解
2016/06/18 Javascript
js判断是否为空和typeof的用法(详解)
2016/10/07 Javascript
bootstrap3中container与container_fluid外层容器的区别讲解
2017/12/04 Javascript
js字符串处理之绝妙的代码
2019/04/05 Javascript
基于vue实现滚动条滚动到指定位置对应位置数字进行tween特效
2019/04/18 Javascript
JavaScript Canvas编写炫彩的网页时钟
2019/10/16 Javascript
vue+vuex+axios从后台获取数据存入vuex,组件之间共享数据操作
2020/07/31 Javascript
JS中箭头函数与this的写法和理解
2021/01/14 Javascript
Python的gevent框架的入门教程
2015/04/29 Python
Python实现简单的代理服务器
2015/07/25 Python
Python实现的中国剩余定理算法示例
2017/08/05 Python
情人节快乐! python绘制漂亮玫瑰
2020/08/18 Python
python3 tcp的粘包现象和解决办法解析
2019/12/09 Python
python GUI库图形界面开发之PyQt5信号与槽的高级使用技巧(自定义信号与槽)详解与实例
2020/03/06 Python
JetBrains PyCharm(Community版本)的下载、安装和初步使用图文教程详解
2020/03/19 Python
Python爬虫逆向分析某云音乐加密参数的实例分析
2020/12/04 Python
利用CSS3的transform做的动态时钟效果
2011/09/21 HTML / CSS
捷克时尚网上商店:OTTO
2018/03/15 全球购物
商务专员岗位职责
2013/11/23 职场文书
总裁办公室主任职责
2014/01/02 职场文书
同学聚会策划方案
2014/06/06 职场文书
党性分析自查总结
2014/10/14 职场文书