vue3+ts组件封装踩坑(install引发的类型错误)

14,125 阅读2分钟

最近在琢磨基于vue3+ts封装自己的组件,中间碰到一个类型定义的坑,在此做个记录和分享

组件示例

举一个最简单的例子,要封装一个Hello组件,目录如下:

Hello
├── Hello.vue
└── Hello.ts
// Hello.ts
import { App } from 'vue'
import Hello from './Hello.vue'
type SFCWithInstall<T> = T & { install(app: App): void; } // 这是从element-puls取过来的类型

Hello.install = (app: App): void => {
  app.component(Hello.name, Hello)
}

const _Hello: SFCWithInstall<typeof Hello> = Hello

export default _Hello

报错

定义组件install方法,导出组件,我这里的写法和element-puls组件一样。到这里就出现了类型报错,错误信息:

不能将类型“DefineComponent<{}, {}, any, ComputedOptions, MethodOptions, ComponentOptionsMixin, ComponentOptionsMixin, ... 4 more ..., {}>”分配给类型“SFCWithInstall<DefineComponent<{}, {}, any, ComputedOptions, MethodOptions, ComponentOptionsMixin, ComponentOptionsMixin, ... 4 more ..., {}>>”
  类型 "ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> & ComponentOptionsBase<Readonly<{} & ... 1 more ... & {}>, ... 8 more ..., {}> & VNodeProps & AllowedComponentProps & ComponentCustomProps" 中缺少属性 "install",但类型 "{ install(app: App<any>): void; }" 中需要该属性ts(2322)

image.png

问题分析

毫无疑问,Hello.ts的代码是没有问题的。出现这种类型报错的原因实际来自于这一行:

import Hello from './Hello.vue'

这是一个非常隐蔽的问题,是由于ts对于vue类型的识别导致的。搭建项目之初,vue类型声明我是从网上copy的,是下面这种:

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

导致我们导入Hello组件时,组件的类型就是普通的组件类型DefineComponent<{}, {}, any>,这个类型中是没有install属性的,所以才会导致后续的报错。

这里定义了新的类型SFCWithInstall<T>没有作用,具体原因还在寻找

解决方案

因此,我们解决这个问题的思路就是在组件类型中补充install属性。做法可以有多种:

// 第1种 使用联合类型
declare module '*.vue' {
  import type { App, DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any> & {
    install(app: App): void
  }
  export default component
}
// 第2种 使用函数返回值类型
// defineComponent函数的返回值类型本身是包含install属性的,这种做法更直观且更贴合组件本身的类型
// 个人更推荐这种
declare module '*.vue' {
  import { defineComponent } from 'vue'
  const component: ReturnType<typeof defineComponent>
  export default component
}

修改后,报错消失。进一步思考,如果Hello组件的类型本身是包含install属性,那么SFCWithInstall<T>是不是就已经不需要了?

// Hello.ts
import { App } from 'vue'
import Hello from './Hello.vue'

Hello.install = (app: App): void => {
  app.component(Hello.name, Hello)
}

export default Hello

没有问题,不报错,安装组件时也没有报错。

不知道element-puls为什么要保留这个类型