一、前言
027 篇学过 defineModel 的基础用法,033 篇用过 defineExpose 开门——本篇给这两个"新 API"穿上 TS 盔甲:模型 ref 的类型怎么标?父组件拿到的子组件实例怎么声明类型?033 篇"InstanceType 伏笔"今天兑现。
二、defineModel 的类型标注
基本形态:泛型里直接写值的类型:
<!-- 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",一行都不用断言:
<template> <input v-model="model" /> </template>具名模型 + 默认值 + 校验全套:
// v-model:title 场景consttitle=defineModel<string>('title',{default:'未命名',validator:(v:string)=>v.length<=20})// v-model:id 场景(数字)constid=defineModel<number>('id')父组件侧无需任何额外标注——v-model 绑定的 ref 类型对上即可:
<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 本身不需要泛型——传入对象即推断):
<!-- 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 组件>取得实例类型:
<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 版:
<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可以少写一遍变量名对应:
import{useTemplateRef}from'vue'// 泛型直接标 DOM 类型,字符串对应模板里的 ref 名constinputRef=useTemplateRef<HTMLInputElement>('inputRef')两种写法都对,项目统一即可。
五、综合实战:类型化的表单遥控器
033 篇 EmailForm 的完整 TS 化(表单 + validate/reset + 父组件遥控):
<!-- 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><!-- 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)实战。