在 Vue 3 + TypeScript 的项目中,为组件定义明确的 Props 和 Emits 类型,不仅能带来编辑器的智能提示和自动补全,还能在编译阶段就拦截掉大部分“传错参数”的问题。下面以 <script setup> 语法为主,展示最实用的写法。
Props 类型定义
基础用法:使用类型参数
defineProps 可以直接接收一个泛型参数,这个参数可以是一个接口、类型别名或内联类型字面量:
<script setup lang="ts">
interface Props {
title: string
count: number
disabled?: boolean
}
// 泛型传入接口,获得带类型的 props 对象
const props = defineProps<Props>()
</script>
使用时,props.title 会被推断为 string,props.disabled 为 boolean | undefined。父组件传值时,TypeScript 会校验必填项和类型。
带默认值:withDefaults
如果某些 prop 希望有默认值,使用 withDefaults 包裹,第二个参数传入一个默认值对象:
<script setup lang="ts">
interface Props {
title: string
count?: number
type?: 'primary' | 'default'
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
type: 'default'
})
</script>
withDefaults 的第二个参数必须是字面量对象,不能是外部函数或变量,这是 Vue 编译器的限制。它可以正确处理联合类型与可选属性的默认值。
复杂类型:使用类型工具
Prop 的类型可以是任意 TypeScript 类型,包括嵌套对象、数组、函数签名等:
<script setup lang="ts">
interface User {
name: string
age: number
}
interface Props {
user: User
tags: string[]
callback?: (id: number) => void
}
defineProps<Props>()
</script>
注意:Vue 的运行时校验不会校验这些复杂类型的内部结构,它只会检查属性是否存在,类型安全完全由 TypeScript 在编译期保证。如果你需要在运行时做细粒度校验,可以使用 validator 配合对象语法,但那会丢失纯泛型的简洁性,通常推荐在组合式函数或 Hooks 中进行自定义校验。
Emits 类型定义
defineEmits 同样支持泛型,但它的类型签名是一个函数签名联合,描述每个事件的名字和它携带的参数类型。
基础用法
<script setup lang="ts">
// 定义两个可能发出的事件
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
(e: 'submit', data: { id: number; name: string }): void
}>()
// 使用时触发事件
emit('update:modelValue', '新值')
emit('submit', { id: 1, name: 'Alice' })
</script>
e 参数代表事件名,后面的参数是传给父组件的值类型。函数返回值统一为 void。
简洁语法(Vue 3.3+)
从 Vue 3.3 开始,defineEmits 支持更简短的对象语法,通过 Record<事件名, 参数列表类型> 来定义:
<script setup lang="ts">
const emit = defineEmits<{
'update:modelValue': [value: string]
submit: [data: { id: number; name: string }]
}>()
</script>
这个写法与函数签名等价,视觉上更清晰。
选项式 API 中的类型定义
如果你仍在使用选项式 API,可以使用 PropType 工具类型和 defineComponent:
import { defineComponent, PropType } from 'vue'
interface User {
name: string
age: number
}
export default defineComponent({
props: {
user: {
type: Object as PropType<User>,
required: true
},
count: {
type: Number,
default: 0
}
},
emits: {
// 校验函数也能辅助类型推断,但通常配合 defineComponent 全局类型推导
update: (payload: User) => true
}
})
不过这种写法相对繁琐,建议在新项目中优先使用 <script setup> 组合式语法。
最佳实践提醒
- Props 只读:所有 prop 在子组件内部都不应该被直接修改,TypeScript 会帮你标记为只读(
readonly),强制单向数据流。 - 复杂默认值:如果默认值是一个需要计算的对象或数组,尽量在
withDefaults中定义简单字面量,或者在组件内部用computed来兜底,避免引用类型共享带来的副作用。 - 类型与运行时可兼得:如果既想保持泛型的简洁,又需要运行时警告,可以额外在开发环境下使用自定义的
validator或引入 Zod 等 schema 库做校验,不过这并不是常见需求。
通过这套类型定义方式,你的组件接口会变得像一份清晰的“使用说明书”:父组件传什么、子组件发出什么,IDE 和编译器都会帮你严格把关,大大降低多人协作时因传参错误导致的 bug。