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>
看起来没问题,但实际开发中有几个难受的点:
PropType是什么,为什么要强制类型断言?- 用
PropType后,IDE 的补全还是不对 - 默认值
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 是编译时宏,它会:
- 给
size和showAvatar补充运行时的默认值 - 在 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 类型处理,关键点在于:
- 优先用
defineProps泛型版本——类型推导更准确,IDE 支持更好 - 带默认值的 props 用
withDefaults——运行时有默认值,类型层面也不再可选 - 需要验证函数时只能用
defineComponent+PropType——泛型版本不支持验证器 - union type 和复杂类型要显式标注——TypeScript 的类型推断在嵌套场景下可能不够精确
- 导出 Props 接口供复用——其他地方需要引用组件 props 类型时会很方便
虽然写起来比 JavaScript 版本的 props 多一些代码,但类型安全带来的收益是值得的。编译时发现问题比运行时出问题好太多了。
