人人都会AI编程

18.1 组件 Props、Emits 的类型定义

更新时间:2026-07-09

在 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 会被推断为 stringprops.disabledboolean | 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。