详解在Vue中使用TypeScript的一些思考(实践)
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 类型:
这意味着我们可以使用 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 时,便能得到相关提示:
然而,你必须以函数返回值的形式断言,并不能直接断言:
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 第二部分可知,除传入原生构造函数外,我们还可传入自定义类:
此外,这里有个 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:
而类做为 typescript 特殊的存在(它既可以作为类型,也可以作为值),当我们使用 vue-class-component
并通过 $refs
绑定为子类组件时,便能获取子组件上暴露的类型信息:
导入 .vue 时,为什么会报错?
当你在 vue 中使用 typescript 时,所遇到的第一个问题即是在 ts 文件中找不到 .vue 文件,即使你所写的路径并没有问题:
在 typescript 中,它仅识别 js/ts/jsx/tsx 文件,为了让它识别 .vue 文件,我们需要显式告诉 typescript,vue 文件存在,并且指定导出 vueconstructor:
declare module '*.vue' { import vue from 'vue' export default vue }
但是,这引起了另一个问题,当我们导入一个并不存在的 .vue 文件时,也能通过编译:
是的,这在情理之中。
当我尝试在 .vue 文件中导入已存在或者不存在的 .vue 文件时,却得到不同的结果:
文件不存在时:
文件存在时:
文件不存在时,引用 vue 的声明文件。文件存在时,引用正确的文件定义。
这让人很困惑,而这些都是 vetur 的功劳。
在这个 pr 下,我找到相关解答:这个 pr 里,vetur 提供解析其他 .vue 文件的功能,以便能获取正确的信息,当 .vue 文件不存在时,会读取 .d.ts 里的信息。
参考
上一篇: 做你的情郎
下一篇: 18W快充!99元小米二合一移动电源图赏