用 TypeScript 类型系统为 Vue 3 组件建立编译时 API 契约
一、问题:为什么运行时 PropTypes 已经不够用了
Vue 2 时代我们这样定义 Props:
// Vue 2 Options API
props: {
userId: { type: Number, required: true },
role: { type: String, default: 'viewer' }
}
这在大型项目中有两个致命问题:
- 父组件传错类型时只在控制台 warning,CI 不会红
- 重构字段名后,所有引用处的类型变成
any,IDE 哑火
Vue 3 + <script setup lang="ts"> 的组合提供了新答案:把类型安全前置到编译期。
二、Props:从 Runtime 校验到泛型约束
❌ 错误示范:丢失了类型约束
// Bad: 用 Vue 2 的习惯写 Vue 3
const props = defineProps({
items: Array, // 类型推断为 any[]
config: Object // 类型推断为 Record<string, any>
})
props.items.forEach(item => {
item.name // ❌ 没有智能提示,重构时不会报错
})
✅ 正确示范:泛型 + interface
interface Item {
id: number
name: string
status: 'active' | 'archived'
}
interface ListConfig {
pageSize: number
sortBy: 'name' | 'createdAt'
}
const props = defineProps<{
items: Item[]
config?: ListConfig
onSelect?: (item: Item) => void // 函数签名也可约束
}>()
// 现在 IDE 会给出完整的字段补全
props.items.forEach(item => {
item.status === 'archived' // ✅ 'active' | 'archived' 自动补全
})
当父组件传 items=[{ id: '1' }] 时,编译直接报错而非运行时报 warning。
withDefaults 处理可选默认值
const props = withDefaults(defineProps<{
items: Item[]
config?: ListConfig
}>(), {
config: () => ({ pageSize: 20, sortBy: 'createdAt' })
})
// config 被自动推断为不可为 undefined
console.log(props.config.pageSize) // ✅ 类型安全
三、Emits:强类型事件签名
Vue 3.3+ 的 defineEmits 支持字面量函数签名:
❌ 无类型约束
const emit = defineEmits(['update', 'delete'])
emit('update', 'wrong-payload') // ❌ 第二个参数没有类型检查
✅ 完整签名约束
const emit = defineEmits<{
(e: 'update:item', item: Item): void
(e: 'delete', id: number): void
(e: 'batch-archive', ids: number[], reason: string): void
}>()
emit('delete', 'abc') // ❌ Argument of type 'string' is not assignable to 'number'
emit('batch-archive', [1, 2]) // ❌ 缺少 reason 参数
emit('batch-archive', [1, 2], '业务下线') // ✅
在父组件通过 v-on 使用时事件处理器参数也会得到精确推导:
<ItemList @delete="(id) => { /* id 类型自动推断为 number */ }" />
四、Template Refs:类型安全的 DOM/组件引用
import { ref } from 'vue'
import MyModal from './MyModal.vue'
// 元素 ref
const inputRef = ref<HTMLInputElement | null>(null)
// 子组件 ref
const modalRef = ref<InstanceType<typeof MyModal> | null>(null)
function focusInput() {
inputRef.value?.focus() // ✅ 类型安全
}
function openModal() {
modalRef.value?.open() // ✅ 自动推导 MyModal 暴露的方法
}
坑点提示:ref<HTMLInputElement>() 不加 | null 初始值会导致类型报错,必须显式标明 null 联合。
五、Provide / Inject 的类型桥梁
祖孙组件传值最怕字符串 key 拼写错误。用 InjectionKey<T> 彻底解决:
// types.ts —— 集中管理注入 Token
import type { InjectionKey, Ref } from 'vue'
export interface UserSession {
id: number
name: string
permissions: string[]
}
export const USER_KEY: InjectionKey<Ref<UserSession>> = Symbol('user')
export const THEME_KEY: InjectionKey<'light' | 'dark'> = Symbol('theme')
// 祖先组件
import { provide, ref } from 'vue'
import { USER_KEY } from './types'
const session = ref<UserSession>({
id: 1, name: 'Alex', permissions: ['read', 'write']
})
provide(USER_KEY, session) // ✅ 类型匹配
// 子组件
import { inject } from 'vue'
import { USER_KEY } from './types'
const session = inject(USER_KEY)
// session 类型自动推导为 Ref<UserSession> | undefined
如果用非 Symbol key 的魔法字符串,类型会丢失:
// Bad
provide('user', { name: 'Alex' })
const user = inject('user') // ❌ unknown
六、将类型约束辐射到整个组件树
每个组件导出 Props/Emits 类型,业务层组装:
// ItemList.vue —— 暴露类型
export type ItemListProps = InstanceType<typeof import('./ItemList.vue')>['$props']
export type ItemListEmits = InstanceType<typeof import('./ItemList.vue')>['$emit']
在组合组件中复用:
// Dashboard.vue
import type ItemList from './ItemList.vue'
const listProps = computed<InstanceType<typeof ItemList>['$props']>(() => ({
items: filteredItems.value,
config: { pageSize: 10, sortBy: 'name' }
}))
重构 ItemList 的 Props 时,Dashboard 相关的编译错误会立刻全量暴露。
七、小结
Vue 3 的 Composition API 提供了类型安全的全部基础设施,关键是用好它:
| 场景 | 方案 | 开销 |
|---|---|---|
| Props | defineProps<T>() + withDefaults | 零运行时 |
| Emits | defineEmits<{...}>() 字面量签名 | 零运行时 |
| Template Refs | ref<HTMLElement | null>(null) | 零运行时 |
| 跨级注入 | InjectionKey<T> + Symbol | 零运行时 |
| 类型导出 | InstanceType<typeof Component> | 零运行时 |
全部零运行时开销,全部在编译期绑定。比依赖测试用例抓 bug 更可靠——因为代码根本过不了 tsc。
0 评论
评论区
登录 后参与评论