一、前言
027 篇学过 defineModel 的基础用法,033 篇用过 defineExpose 开门------本篇给这两个"新 API"穿上 TS 盔甲:模型 ref 的类型怎么标?父组件拿到的子组件实例怎么声明类型?033 篇"InstanceType 伏笔"今天兑现。
二、defineModel 的类型标注
基本形态:泛型里直接写值的类型:
vue
<!-- src/components/TsInput.vue -->
<template>
<input :value="model" @input="model = ($event.target as HTMLInputElement).value" />
</template>
<script setup lang="ts">
// Ref<string | undefined>:父组件可以不传
const model = defineModel<string>()
// 必填模型:父组件必须 v-model
const requiredModel = defineModel<string>({ required: true })
// 带默认值:类型自动收窄为 string
const withDefault = defineModel<string>({ default: '' })
</script>
as HTMLInputElement 是本篇第一个类型断言:$event.target 默认类型是 EventTarget(没有 .value),"我确定它是 input 元素" 就用 as 断言------比事件处理器里手写类型更省事。模板里更常见的写法其实还是 v-model="model",一行都不用断言:
vue
<template>
<input v-model="model" />
</template>
具名模型 + 默认值 + 校验全套:
ts
// v-model:title 场景
const title = defineModel<string>('title', {
default: '未命名',
validator: (v: string) => v.length <= 20
})
// v-model:id 场景(数字)
const id = defineModel<number>('id')
父组件侧无需任何额外标注------v-model 绑定的 ref 类型对上即可:
vue
<template>
<!-- titleRef 是 Ref<string>,和子组件 model 类型匹配 -->
<TsInput v-model="titleRef" />
<TsInput v-model:id="idRef" />
</template>
类型对不上(比如子组件要 number 你绑了 string)编辑器直接红线------组件的 v-model 协议也被编译器看守了。
三、defineExpose 与 InstanceType:给实例开门挂牌
033 篇的 JS 版本里,父组件 const panelRef = ref(null) 一把梭。TS 版本要回答两个问题:子组件暴露了什么类型?父组件的 ref 该标什么?
先看子组件(defineExpose 本身不需要泛型------传入对象即推断):
vue
<!-- src/components/CountPanel.vue -->
<template>
<div>
<p>内部计数:{{ count }}</p>
<button @click="count++">内部 +1</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
function reset(): void {
count.value = 0
}
// 传入对象即推断,无需手写泛型;想强约束也可以 defineExpose<{...}>({...})
defineExpose({ count, reset })
</script>
父组件用 InstanceType<typeof 组件> 取得实例类型:
vue
<template>
<CountPanel ref="panelRef" />
<button @click="remote">父组件遥控</button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import CountPanel from './components/CountPanel.vue'
// ★ 核心句式:组件类型 → 实例类型
const panelRef = ref<InstanceType<typeof CountPanel> | null>(null)
function remote() {
if (!panelRef.value) return
panelRef.value.reset() // ✅ 有提示:reset(): void
const c: number = panelRef.value.count // ✅ count 自动解包成 number
console.log(c)
}
</script>
拆解这个句式:
typeof CountPanel:拿到组件定义的类型(typeof 在类型层面取"变量"的类型)InstanceType<...>:工具类型,取"这个组件实例化后"的类型------自动包含 defineExpose 的所有成员| null:挂载前是 null,手动加上(或用可选链panelRef.value?.reset())
defineExpose 没暴露的成员,类型上同样拿不到------运行时的"门"和类型上的"门"是同一扇,编译器替 033 篇的规矩执法。
【截图位置:panelRef.value. 后自动提示 count/reset 的 IDE 截图】
四、DOM ref 的类型标注
拿 DOM 元素(033 篇 input 聚焦案例)的 TS 版:
vue
<template>
<input ref="inputRef" placeholder="自动聚焦" />
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// DOM 类型:HTMLInputElement / HTMLDivElement / HTMLCanvasElement...
const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => {
inputRef.value?.focus() // 可选链,挂载前调用也不炸
})
</script>
DOM 类型速查:input→HTMLInputElement、div→HTMLDivElement、img→HTMLImageElement、video→HTMLVideoElement、通用容器兜底 HTMLElement。命名规律:HTML + 标签名首字母大写 + Element。
Vue 3.5+ 的 useTemplateRef 可以少写一遍变量名对应:
ts
import { useTemplateRef } from 'vue'
// 泛型直接标 DOM 类型,字符串对应模板里的 ref 名
const inputRef = useTemplateRef<HTMLInputElement>('inputRef')
两种写法都对,项目统一即可。
五、综合实战:类型化的表单遥控器
033 篇 EmailForm 的完整 TS 化(表单 + validate/reset + 父组件遥控):
vue
<!-- src/components/EmailForm.vue -->
<template>
<div>
<input v-model.trim="email" placeholder="邮箱" />
<p v-if="error" class="err">{{ error }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const email = ref('')
const error = ref('')
function validate(): boolean {
const ok = /^[\w.-]+@[\w-]+(\.[\w-]+)+$/.test(email.value)
error.value = ok ? '' : '邮箱格式不正确'
return ok
}
function reset(): void {
email.value = ''
error.value = ''
}
defineExpose({ validate, reset })
</script>
<style scoped>
.err { color: #e74c3c; font-size: 12px; }
</style>
vue
<!-- App.vue -->
<template>
<EmailForm ref="formRef" />
<button @click="onSubmit">提交</button>
<button @click="formRef?.reset()">重置</button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import EmailForm from './components/EmailForm.vue'
const formRef = ref<InstanceType<typeof EmailForm> | null>(null)
function onSubmit() {
// validate 的返回值类型 boolean,写错判断逻辑编译器能看出来
if (formRef.value?.validate()) {
console.log('校验通过,提交')
}
}
</script>
对比 033 篇 JS 版:功能一致,但 formRef.value.validateXxx()(方法名拼错)、validate() 返回值当数字用,全部变成编译期错误。
【截图位置:表单校验失败提示 + 提交成功日志的运行效果】
六、踩坑记录
- InstanceType 拿不到成员:子组件没 defineExpose 或没暴露该方法------类型层面同样"关门",先检查子组件开了哪些门
- ref 忘标 | null :
ref<InstanceType<typeof Child>>(null)报错(null 不在类型里)------模板挂载前是 null,类型必须含 null - 模板里 $event.target as 写错:断言目标写 HTMLElement 没有 .value------input 用 HTMLInputElement,select 用 HTMLSelectElement
- defineModel 泛型标了基本类型却绑对象 :
defineModel<string>()配v-model="userObj"红线------模型类型必须与父组件数据一致 - useTemplateRef 找不到(报没有这个导出) :它是 3.5+ API,老版本用
ref<HTMLInputElement | null>(null)经典写法 - as 滥用:实在拿不准才断言;到处 as any 等于卸甲------断言是"签名担保"不是"闭嘴器"
七、今日小结
- defineModel<T>():泛型标值类型,required/default/validator 全支持,具名模型第二参数传名字
- defineExpose 传对象即推断;父组件用
ref<InstanceType<typeof 子组件> | null>(null)拿实例类型 - DOM ref 标 HTMLXxxElement;3.5+ 可用 useTemplateRef<T>('名字')
- as 断言用在没有类型的边界($event.target),边界内全靠接口和推断
下篇预告
TS 篇压轴:泛型组件------同一个列表组件,给用户列表用 item 是 User,给商品列表用 item 是 Product,类型还全程精准。下一篇 044 泛型组件(generic attribute)实战。