likes
comments
collection
share

vite3+vue3+ts+pinia + Naive UI 项目实战 —— 使用 ref 引用组件时如何标注类型

作者站长头像
站长
· 阅读数 49

接前四篇 《项目创建与初始配置》 《国际化配置》 《数据表格与渲染函数》 《区分运行环境并定义 baseURL》, 本篇来说说在后台项目中遇到的一个小问题及解决办法。

问题阐述

在构建的项目中,有时我们需要在父组件中调用子组件通过 defineExpose() 对外暴露的方法。比如我在做登录页面时,将登录按钮放在父组件中,然后把登录表单封装成了子组件 LoginForm.vue,并在里面定义了登录相关方法 validateFormModel。示意图如下:

vite3+vue3+ts+pinia + Naive UI 项目实战 —— 使用 ref 引用组件时如何标注类型

也就是说,当父组件中的登录按钮被点击时,需要触发子组件的 validateFormModel 方法。实现思路即为给子组件 LoginForm 添加一个引用 ref="loginFormRef",接着给按钮绑定一个方法 handleSubmit,然后在方法内通过 loginFormRef.value?.validateFormModel() 调用:

<!-- 父组件,省略部分代码 -->
<script lang="ts" setup>
import { ref } from 'vue'
import LoginForm from './components/LoginForm.vue'

const loginFormRef = ref() // 这里还有问题,下文中解决
function handleSubmit() {
  loginFormRef.value?.validateFormModel()
}
</script>
<template>
  <n-dialog-provider>
    <LoginForm ref="loginFormRef" />
  </n-dialog-provider>
  <n-button @click="handleSubmit">
    登录
  </n-button>
</template>

在给 loginFormRef 赋值时,因为 ref() 是没有传入参数的,所以无法直接像 const str = ref('hello') 这样可以通过类型推导得知 str 为字符串类型,然后限制 str 拥有的属性和可以使用的方法。查看 ref 的类型声明:

// node_modules\@vue\reactivity\dist\reactivity.d.ts
export declare function ref<T = any>(): Ref<T | undefined>;

得知我们可以通过指定具体的泛型类型的方式来标注 loginFormRef 的类型。那么问题来了,loginFormRef 指向的 loginForm 是我们自己定义的一个组件,如何获取它的类型呢?

解决办法

查阅 vue3 的官方文档,可以看到下面这样的内容: vite3+vue3+ts+pinia + Naive UI 项目实战 —— 使用 ref 引用组件时如何标注类型 依葫芦画瓢,我就知道在定义 loginFormRef 时可以写成

const loginFormRef = ref<InstanceType<typeof LoginForm> | null>()

如此,ts 就会知道 loginFormRef 的具体类型,会给我们提供提示与检查了:

vite3+vue3+ts+pinia + Naive UI 项目实战 —— 使用 ref 引用组件时如何标注类型

虽然知道了解决办法,为了知其所以然,我们再来看看所谓的 ts 内置的工具类型 InstanceType 到底是啥?

InstanceType

ts 为我们封装了一些工具类型,它们都是可以全局使用的,所以无需进行导入,InstanceType<Type> 就是其中之一。ts 官方文档的介绍如下:

Constructs a type consisting of the instance type of a constructor function in Type.

直译的话有点绕,我大概理解为,构造一个类型,这个类型是由类型为 Type 的构造函数的实例类型组成的。

借助 infer 手写实现一个 InstanceType

我们可以自己实现一个 InstanceType 叫做 MyInstanceType,这其实也是 ts 的一个类型体操了:

type MyInstanceType<T extends new (...args: any[]) => any> = T extends new (
  ...args: any[]
) => infer Return
  ? Return
  : never

注释:

  1. <T extends new (...args: any[]) => any>:是使用 extends 对泛型类型 T 做一个约束,让其必须为构造函数;
  2. 等号右边的 T extends new (...args: any[]) => infer Return ? Return : never 是使用了 infer 关键字推断出构造函数的返回值,因为等号左边已经对 T 进行了约束,所以 T extends new (...args: any[]) => any ? any : never 的结果必然是返回 any,那么我们就可以用 infer Return 来推断返回值的类型,然后返回 Return。关于 infer 的使用可详见文档

总结归纳

现在我们知道了,InstanceType<Type> 只要给 Type 传入一个构造函数的类型,就能得到该构造函数的实例的类型,那么 LoginForm 明明是我们定义的组件,为啥 typeof LoginForm 也能作为 Type 传入呢?

其实在 vue 中,我们使用了组件,比如 <LoginForm />,就可以看成是 LoginForm 创建了一个实例,在 setup 语法糖出现之前,我们创建组件是这样写的:

export default defineComponent({
  // ...
  setup() {}
})

即组件的类型其实都是 DefineComponent,如果查看源码会发现 DefineComponent 的定义相对比较复杂:

// node_modules\@vue\runtime-core\dist\runtime-core.d.ts
export declare type DefineComponent<PropsOrPropOptions = {}, RawBindings = {}, D = {}, C extends ComputedOptions = ComputedOptions, M extends MethodOptions = MethodOptions, Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, Extends extends ComponentOptionsMixin = ComponentOptionsMixin, E extends EmitsOptions = {}, EE extends string = string, PP = PublicProps, Props = Readonly<PropsOrPropOptions extends ComponentPropsOptions ? ExtractPropTypes<PropsOrPropOptions> : PropsOrPropOptions> & ({} extends E ? {} : EmitsToProps<E>), Defaults = ExtractDefaultPropTypes<PropsOrPropOptions>> = ComponentPublicInstanceConstructor<CreateComponentPublicInstance<Props, RawBindings, D, C, M, Mixin, Extends, E, PP & Props, Defaults, true> & Props> & ComponentOptionsBase<Props, RawBindings, D, C, M, Mixin, Extends, E, EE, Defaults> & PP;

最终是等于几个类型的联合类型,其中有一个是 ComponentPublicInstanceConstructor

// node_modules\@vue\runtime-core\dist\runtime-core.d.ts
declare type ComponentPublicInstanceConstructor<T extends ComponentPublicInstance<Props, RawBindings, D, C, M> = ComponentPublicInstance<any>, Props = any, RawBindings = any, D = any, C extends ComputedOptions = ComputedOptions, M extends MethodOptions = MethodOptions> = {
  __isFragment?: never;
  __isTeleport?: never;
  __isSuspense?: never;
  new(...args: any[]): T;
};

ComponentPublicInstanceConstructor 的类型声明里可以看到有构造签名 new(...args: any[]): T; 所以我们可以把 typeof LoginForm作为泛型的具体类型传给 InstanceType<Type>,从而拿到 LoginForm 的具体类型。

转载自:https://juejin.cn/post/7196926405250252861
评论
请登录