组件类型-Props-Emits-Ref

📅 2026/7/3 4:23:06
组件类型-Props-Emits-Ref
文章目录前言一、Props 类型声明1.1 运行时对象写法1.2 泛型写法1.3 withDefaults 默认值1.4 Props 类型导出给父组件二、Props 解构与响应性2.1 不建议直接普通解构2.2 使用 toRefs 保留响应性三、Emits 类型声明3.1 数组写法3.2 函数重载写法3.3 命名元组写法3.4 父组件监听事件四、v-model 的类型4.1 modelValue update:modelValue4.2 多个 v-model五、模板 Ref 类型5.1 DOM ref5.2 组件 ref六、defineExpose 暴露方法类型七、Slot 类型简单了解八、面试聚焦8.1 defineProps 泛型写法为何推荐8.2 Props 解构会不会丢失响应性8.3 defineEmits 如何限制参数8.4 模板 ref 为什么要写 null九、易混淆点十、思考与练习总结前言在 Vue 3 TypeScript 项目中组件类型是最常写、也最容易踩坑的一块。Props 写得不清楚父组件传参容易出错Emits 没有约束事件名和参数容易写散模板 ref 没有类型调用 DOM 或子组件方法时经常只能any或非空断言硬顶。本篇围绕组件开发中最高频的三类类型展开defineProps父传子参数如何声明类型与默认值defineEmits子传父事件如何约束事件名与参数ref/ 模板 ref如何拿到 DOM、组件实例与暴露方法的类型一、Props 类型声明1.1 运行时对象写法Vue 仍支持传统运行时对象写法适合需要运行时校验、默认值、必填项的场景script setup langts const props defineProps({ title: { type: String, required: true }, count: { type: Number, default: 0 }, disabled: { type: Boolean, default: false } }) console.log(props.title) /script这种写法的优点是保留 Vue 的运行时 props 校验缺点是复杂对象、联合类型、字面量类型表达起来不够自然。1.2 泛型写法在script setup langts中更推荐用泛型声明 Propsscript setup langts interface UserCardProps { id: number name: string avatar?: string role: admin | user | guest } const props definePropsUserCardProps() console.log(props.name) /script泛型写法更接近 TS 的类型系统适合复杂对象类型联合类型字面量类型从其他文件导入类型组件之间复用 Props 类型// types/user.tsexportinterfaceUser{id:numbername:stringemail?:string}script setup langts import type { User } from /types/user defineProps{ user: User size?: small | medium | large }() /script注意import type只导入类型编译后不会进入运行时代码推荐在 TS 项目中养成这个习惯。1.3 withDefaults 默认值泛型写法没有运行时对象里的default字段如果要设置默认值需要配合withDefaultsscript setup langts interface Props { title: string count?: number size?: small | medium | large tags?: string[] } const props withDefaults(definePropsProps(), { count: 0, size: medium, tags: () [] }) /script这里有两个重点count?、size?表示父组件可以不传。数组、对象默认值建议用函数返回避免引用共享。withDefaults后组件内部读取props.count时TS 会知道它已经有默认值不再是number | undefined。1.4 Props 类型导出给父组件如果某个组件的 Props 会被父组件、配置项或测试复用可以导出类型script setup langts export interface UserCardProps { id: number name: string role?: admin | user } withDefaults(definePropsUserCardProps(), { role: user }) /script父组件或其他模块可以复用importtype{UserCardProps}from/components/UserCard.vueconstdefaultUser:UserCardProps{id:1,name:张三,role:admin}这种写法在中后台项目中很实用尤其是表格列配置、弹窗表单配置和组件测试。二、Props 解构与响应性2.1 不建议直接普通解构defineProps返回的是响应式 props 对象。普通解构在一些写法中容易丢失响应性script setup langts const props defineProps{ keyword: string }() // 推荐通过 props.keyword 使用 console.log(props.keyword) /script如果你需要在逻辑里频繁使用某个字段优先保持props.xxx语义清楚也不容易误判响应性。2.2 使用 toRefs 保留响应性需要解构时可以用toRefsscript setup langts import { toRefs, watch } from vue const props defineProps{ keyword: string page: number }() const { keyword, page } toRefs(props) watch(keyword, (val) { console.log(keyword changed:, val) }) console.log(page.value) /scripttoRefs(props)得到的是Ref所以在script中需要.value模板中自动解包。三、Emits 类型声明3.1 数组写法最简单的写法只限制事件名script setup langts const emit defineEmits([close, submit]) emit(close) emit(submit) /script这种写法能限制事件名但不能限制事件参数。比如submit到底要不要传表单数据TS 并不知道。3.2 函数重载写法传统类型写法可以用函数重载描述不同事件script setup langts interface FormData { username: string password: string } const emit defineEmits{ (e: close): void (e: submit, data: FormData): void (e: change, value: string | number): void }() emit(close) emit(submit, { username: admin, password: 123456 }) emit(change, enabled) /script优点是兼容性好很多老项目和库类型里都能看到这种写法。3.3 命名元组写法Vue 3.3 支持更简洁的写法script setup langts interface FormData { username: string password: string } const emit defineEmits{ close: [] submit: [data: FormData] change: [value: string | number] }() emit(close) emit(submit, { username: admin, password: 123456 }) emit(change, 1) /script这类写法更像“事件名 → 参数列表”的映射读起来直观也适合团队统一规范。3.4 父组件监听事件子组件!-- UserForm.vue -- script setup langts interface UserForm { name: string age: number } const emit defineEmits{ submit: [form: UserForm] cancel: [] }() const onSubmit () { emit(submit, { name: 张三, age: 18 }) } /script template button clickonSubmit提交/button button clickemit(cancel)取消/button /template父组件script setup langts import UserForm from ./UserForm.vue const handleSubmit (form: { name: string; age: number }) { console.log(form.name, form.age) } /script template UserForm submithandleSubmit cancelconsole.log(cancel) / /template在 IDE 中事件名、参数数量、参数类型都能得到提示。四、v-model 的类型4.1 modelValue update:modelValueVue 3 中组件上的v-model本质是父传子modelValue子传父update:modelValue!-- BaseInput.vue -- script setup langts defineProps{ modelValue: string }() const emit defineEmits{ update:modelValue: [value: string] }() /script template input :valuemodelValue inputemit(update:modelValue, ($event.target as HTMLInputElement).value) / /template父组件script setup langts import { ref } from vue import BaseInput from ./BaseInput.vue const keyword ref() /script template BaseInput v-modelkeyword / /template这里modelValue是string所以父组件的keyword也应是Refstring。4.2 多个 v-model多个v-model会变成不同的 prop 与事件!-- SearchPanel.vue -- script setup langts defineProps{ keyword: string page: number }() const emit defineEmits{ update:keyword: [value: string] update:page: [value: number] }() /script template input :valuekeyword inputemit(update:keyword, ($event.target as HTMLInputElement).value) / button clickemit(update:page, page 1)下一页/button /template父组件template SearchPanel v-model:keywordkeyword v-model:pagepage / /template五、模板 Ref 类型5.1 DOM ref访问 DOM 节点时要把初始值写成null并显式标注元素类型script setup langts import { onMounted, ref } from vue const inputRef refHTMLInputElement | null(null) onMounted(() { inputRef.value?.focus() }) /script template input refinputRef / /template常见 DOM 类型元素类型inputHTMLInputElementtextareaHTMLTextAreaElementselectHTMLSelectElementdivHTMLDivElementformHTMLFormElement口诀模板 ref 初始化为null使用时可选链?.。5.2 组件 ref如果 ref 指向子组件可以用InstanceTypetypeof Compscript setup langts import { ref, onMounted } from vue import UserDialog from ./UserDialog.vue const dialogRef refInstanceTypetypeof UserDialog | null(null) onMounted(() { dialogRef.value?.open() }) /script template UserDialog refdialogRef / /template这要求子组件通过defineExpose暴露方法否则父组件拿不到。六、defineExpose 暴露方法类型子组件默认是关闭的父组件不能随便访问内部变量。需要暴露给父组件的方法用defineExpose!-- UserDialog.vue -- script setup langts import { ref } from vue const visible ref(false) const open () { visible.value true } const close () { visible.value false } defineExpose({ open, close }) /script template div v-ifvisible用户弹窗/div /template父组件script setup langts import { ref } from vue import UserDialog from ./UserDialog.vue const dialogRef refInstanceTypetypeof UserDialog | null(null) const showDialog () { dialogRef.value?.open() } /script template button clickshowDialog打开弹窗/button UserDialog refdialogRef / /template如果想让暴露接口更清晰也可以单独声明类型exportinterfaceUserDialogExpose{open:()voidclose:()void}script setup langts import type { UserDialogExpose } from ./types const exposed: UserDialogExpose { open: () {}, close: () {} } defineExpose(exposed) /script七、Slot 类型简单了解组件类型除了 Props、Emits、Ref还有一个常见点是 Slot。Vue 3.3 可用defineSlots描述插槽参数script setup langts interface Row { id: number name: string } defineProps{ list: Row[] }() defineSlots{ default(props: { row: Row; index: number }): any empty(): any }() /script template div v-iflist.length slot v-for(row, index) in list :keyrow.id :rowrow :indexindex / /div slot v-else nameempty / /template父组件使用时row与index会有类型提示template UserList :listusers template #default{ row, index } {{ index }} - {{ row.name }} /template template #empty 暂无数据 /template /UserList /templateSlot 类型不是本文重点但在封装表格、列表、弹窗 footer 时很常见建议知道defineSlots这个入口。八、面试聚焦8.1 defineProps 泛型写法为何推荐泛型写法更贴合 TypeScript复杂类型表达更自然可复用外部类型也能获得更完整的 IDE 推导。运行时对象写法适合需要 Vue 运行时校验的场景二者按需求选择。8.2 Props 解构会不会丢失响应性直接普通解构容易让后续代码失去对 props 更新的感知。需要解构并保持响应性时使用toRefs(props)简单场景优先直接使用props.xxx。8.3 defineEmits 如何限制参数可以用函数重载写法也可以用 Vue 3.3 的命名元组写法constemitdefineEmits{submit:[data:FormData]close:[]}()这样事件名、参数数量、参数类型都能被 TS 检查。8.4 模板 ref 为什么要写 null组件挂载前 DOM 或子组件实例还不存在所以初始值应为nullconstinputRefrefHTMLInputElement|null(null)使用时通过?.或在onMounted后访问。九、易混淆点运行时 props 写法有 Vue 校验泛型写法更适合 TS 复杂类型。withDefaults用来给泛型 Props 设置默认值数组和对象默认值建议写函数。props普通解构要谨慎需要响应性时用toRefs。defineEmits不只是声明事件名还可以约束参数。v-model本质是modelValueupdate:modelValue。DOM ref 通常写成refHTMLInputElement | null(null)。组件 ref 通常写成refInstanceTypetypeof Comp | null(null)。子组件方法需要defineExpose后父组件 ref 才能访问。十、思考与练习1.defineProps的运行时对象写法和泛型写法有什么区别解析运行时对象写法有 Vue 的运行时校验适合简单类型和默认值泛型写法更适合复杂对象、联合类型、外部类型复用TS 推导更自然。2.泛型 Props 如何设置默认值解析使用withDefaults(definePropsProps(), defaults)数组和对象默认值建议用函数返回。3.为什么不建议随手写const { title } definePropsProps()解析普通解构可能带来响应性误用。需要保持响应性时用toRefs(props)否则优先props.title。4.如何声明一个submit事件参数为{ name: string; age: number }解析constemitdefineEmits{submit:[form:{name:string;age:number}]}()5.父组件如何拿到子组件暴露的open方法解析子组件defineExpose({ open })父组件用refInstanceTypetypeof Child | null(null)获取组件实例并通过childRef.value?.open()调用。总结Props推荐泛型写法复杂类型更清晰默认值用withDefaults。Props 解构优先props.xxx需要响应性解构时用toRefs。Emits用类型约束事件名和参数避免事件写散。v-model本质是 prop update 事件类型要同时约束。模板 refDOM ref 写元素类型组件 ref 写InstanceTypetypeof Comp。defineExpose子组件显式暴露方法父组件才能通过 ref 调用。