vite3+vue3+ts+pinia + Naive UI 项目实战 —— 使用 ref 引用组件时如何标注类型
接前四篇 《项目创建与初始配置》 《国际化配置》 《数据表格与渲染函数》 《区分运行环境并定义 baseURL》, 本篇来说说在后台项目中遇到的一个小问题及解决办法。
问题阐述
在构建的项目中,有时我们需要在父组件中调用子组件通过 defineExpose() 对外暴露的方法。比如我在做登录页面时,将登录按钮放在父组件中,然后把登录表单封装成了子组件 LoginForm.vue,并在里面定义了登录相关方法 validateFormModel。示意图如下:

也就是说,当父组件中的登录按钮被点击时,需要触发子组件的 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 的官方文档,可以看到下面这样的内容:
依葫芦画瓢,我就知道在定义 loginFormRef 时可以写成
const loginFormRef = ref<InstanceType<typeof LoginForm> | null>()
如此,ts 就会知道 loginFormRef 的具体类型,会给我们提供提示与检查了:

虽然知道了解决办法,为了知其所以然,我们再来看看所谓的 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
注释:
<T extends new (...args: any[]) => any>:是使用extends对泛型类型T做一个约束,让其必须为构造函数;- 等号右边的
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