Vue 3 与 TypeScript 结合:props 类型推导与默认值难题

Vue 3 与 TypeScript 结合:props 类型推导与默认值难题

Vue 3 的 defineComponent 配合 TypeScript 本应是完美组合,但 props 的类型定义和默认值配合经常出问题。IDE 报红、类型推导不正确、默认值不生效——这些问题我几乎在每个项目里都会遇到。

根本原因是 Vue 3 的 props 系统是运行时和类型系统两套逻辑并行的,写起来容易,写对不容易。

典型问题场景

假设我们定义一个用户卡片组件:

<script lang="ts">
import { defineComponent, PropType } from 'vue'

interface User {
  id: string
  name: string
  avatar?: string
  role: 'admin' | 'editor' | 'viewer'
  tags: string[]
}

export default defineComponent({
  name: 'UserCard',
  props: {
    user: {
      type: Object as PropType<User>,
      required: true,
    },
    size: {
      type: String,
      default: 'medium',
    },
    showAvatar: {
      type: Boolean,
      default: true,
    },
  },
})
</script>

看起来没问题,但实际开发中有几个难受的点:

  1. PropType 是什么,为什么要强制类型断言?
  2. PropType 后,IDE 的补全还是不对
  3. 默认值 default 是运行时逻辑,TypeScript 不知道这个字段有默认值,所以 size? 这样的类型还是认为它可能是 undefined

更好的方式:用 defineProps 的泛型语法

Vue 3.3+ 引入了 defineProps 的泛型版本,这是目前最好的写法:

<script lang="ts">
interface User {
  id: string
  name: string
  avatar?: string
  role: 'admin' | 'editor' | 'viewer'
  tags: string[]
}

interface Props {
  user: User
  size?: 'small' | 'medium' | 'large'
  showAvatar?: boolean
}

const props = defineProps<Props>()
</script>

这种方式下:

  • 类型直接来自 TypeScript interface,不需要 PropType
  • IDE 补全正常
  • 类型推导正确

但还是有缺角:默认值怎么办?

带默认值的类型安全写法

defineProps 的泛型版本可以通过 withDefaults 支持默认值:

<script lang="ts">
interface User {
  id: string
  name: string
  avatar?: string
  role: 'admin' | 'editor' | 'viewer'
  tags: string[]
}

interface Props {
  user: User
  size?: 'small' | 'medium' | 'large'
  showAvatar?: boolean
  // 复杂类型的默认值需要用工厂函数
  metaInfo?: () => { createdAt: Date; updatedAt: Date }
}

const props = withDefaults(defineProps<Props>(), {
  size: 'medium',
  showAvatar: true,
  // 复杂类型用箭头函数
  metaInfo: () => ({
    createdAt: new Date(),
    updatedAt: new Date(),
  }),
})
</script>

withDefaults 是编译时宏,它会:

  1. sizeshowAvatar 补充运行时的默认值
  2. 在 TypeScript 类型层面,把 size? 变成 size: 'small' | 'medium' | 'large',不再是可选的

默认值的类型限制

withDefaults 时,默认值的类型必须兼容声明的类型:

// 正确的例子
withDefaults(defineProps<{ size?: 'small' | 'medium' }>(), {
  size: 'medium',  // OK,'medium' 是允许的值
})

// 错误的例子
withDefaults(defineProps<{ size?: 'small' | 'medium' }>(), {
  size: 'large',  // ERROR:'large' 不在允许的类型的
})

这其实是个好事——帮你检查默认值是否合法。

复杂 props 的类型处理

union type 的 props

<script lang="ts">
type Status = 'pending' | 'active' | 'blocked'
type Size = 'small' | 'medium' | 'large'

interface Props {
  status: Status
  size?: Size
  // union type 混合 undefined
  description?: string | null
}

const props = withDefaults(defineProps<Props>(), {
  size: 'medium',
  description: null,  // 可以是 null
})
</script>

<template>
  <div :class="[`size-${props.size}`, `status-${props.status}`]">
    <span v-if="props.description">{{ props.description }}</span>
  </div>
</template>

带验证函数的 props

有时候需要在运行时做 props 验证:

<script lang="ts">
import { type PropType } from 'vue'

interface Props {
  user: {
    id: string
    name: string
  }
  scores: number[]
  callback: () => void
}

export default defineComponent({
  name: 'ScoreBoard',
  props: {
    user: {
      type: Object as PropType<Props['user']>,
      required: true,
      validator: (value: Props['user']) => {
        return value.id.length > 0 && value.name.length > 0
      },
    },
    scores: {
      type: Array as PropType<Props['scores']>,
      default: () => [],
    },
    callback: {
      type: Function as PropType<Props['callback']>,
      required: true,
    },
  },
})
</script>

注意这里有个坑:如果你用泛型 defineProps,就无法同时用验证函数。因为泛型版本是纯类型声明,运行时没有 props 对象让你验证。

如果必须验证,只能回到 defineComponent 的写法:

<script lang="ts">
import { defineComponent, PropType } from 'vue'

export default defineComponent({
  name: 'ScoreBoard',
  props: {
    user: {
      type: Object as PropType<{ id: string; name: string }>,
      required: true,
      validator: (value) => {
        return value.id.length > 0
      },
    },
  },
})
</script>

异步 props 的类型处理

有时候 props 是异步加载的,比如从接口获取的数据:

<script lang="ts">
import { defineComponent, PropType } from 'vue'

interface User {
  id: string
  name: string
}

export default defineComponent({
  name: 'UserDetail',
  props: {
    // 可能是 loading 状态
    user: {
      type: [Object, null] as PropType<User | null>,
      default: null,
    },
    loading: {
      type: Boolean,
      default: false,
    },
  },
})
</script>

如果你用泛型版本,可以这样:

<script lang="ts">
interface User {
  id: string
  name: string
}

interface Props {
  user: User | null
  loading: boolean
}

const props = withDefaults(defineProps<Props>(), {
  user: null,
  loading: false,
})
</script>

模板块中访问 props 的类型

<template> 里访问 props 时,Vue 会自动推断类型。但有时候在 TypeScript 代码里需要引用 props 的类型:

<script lang="ts">
interface User {
  id: string
  name: string
}

interface Props {
  user: User
}

const props = defineProps<Props>()

// 如果需要 props 的类型(比如定义 emits)
type PropsType = InstanceType<typeof UserCard>['$props']
// 或者直接提取
type UserCardProps = Props
</script>

更直接的方式是导出 props 类型:

<script lang="ts">
export interface UserCardProps {
  user: User
  size?: 'small' | 'medium' | 'large'
}

const props = defineProps<UserCardProps>()
</script>

然后在其他地方可以复用:

import type { UserCardProps } from '@/components/UserCard.vue'

function useUserCard() {
  const props = defineProps<UserCardProps>()  // 复用类型
  // ...
}

实际项目中的推荐写法

综合以上分析,我现在的项目里这样定义组件 props:

<script lang="ts">
import { defineComponent } from 'vue'

// 定义 Props 接口
export interface ButtonProps {
  /** 按钮文字 */
  label: string
  /** 按钮类型 */
  variant?: 'primary' | 'secondary' | 'danger'
  /** 按钮尺寸 */
  size?: 'small' | 'medium' | 'large'
  /** 是否禁用 */
  disabled?: boolean
  /** 点击回调 */
  onClick?: () => void
}

// 如果用 defineComponent 写法
export default defineComponent({
  name: 'AppButton',
  props: {
    label: {
      type: String,
      required: true,
    },
    variant: {
      type: String as PropType<ButtonProps['variant']>,
      default: 'primary',
    },
    size: {
      type: String as PropType<ButtonProps['size']>,
      default: 'medium',
    },
    disabled: {
      type: Boolean,
      default: false,
    },
  },
  emits: ['click'],
  setup(props, { emit }) {
    function handleClick() {
      if (!props.disabled) {
        emit('click')
      }
    }

    return { handleClick }
  },
})
</script>

如果 Vue 3.3+ 环境,用 withDefaults

<script lang="ts">
export interface ButtonProps {
  label: string
  variant?: 'primary' | 'secondary' | 'danger'
  size?: 'small' | 'medium' | 'large'
  disabled?: boolean
}

const props = withDefaults(defineProps<ButtonProps>(), {
  variant: 'primary',
  size: 'medium',
  disabled: false,
})

const emit = defineEmits<{
  click: []
}>()
</script>

总结

Vue 3 + TypeScript 的 props 类型处理,关键点在于:

  1. 优先用 defineProps 泛型版本——类型推导更准确,IDE 支持更好
  2. 带默认值的 props 用 withDefaults——运行时有默认值,类型层面也不再可选
  3. 需要验证函数时只能用 defineComponent + PropType——泛型版本不支持验证器
  4. union type 和复杂类型要显式标注——TypeScript 的类型推断在嵌套场景下可能不够精确
  5. 导出 Props 接口供复用——其他地方需要引用组件 props 类型时会很方便

虽然写起来比 JavaScript 版本的 props 多一些代码,但类型安全带来的收益是值得的。编译时发现问题比运行时出问题好太多了。

最后更新 4/20/2026, 6:02:32 AM